diff --git a/client/app/assets/less/ant.less b/client/app/assets/less/ant.less index c42a83e2a7..337bc1d744 100644 --- a/client/app/assets/less/ant.less +++ b/client/app/assets/less/ant.less @@ -29,6 +29,7 @@ @import "~antd/lib/card/style/index"; @import "~antd/lib/spin/style/index"; @import "~antd/lib/tabs/style/index"; +@import "~antd/lib/notification/style/index"; @import 'inc/ant-variables'; // Remove bold in labels for Ant checkboxes and radio buttons @@ -247,3 +248,12 @@ .ant-popover { z-index: 1000; // make sure it doesn't cover drawer } + +// flexible width for notifications +.@{notification-prefix-cls} { + width: auto; + + &-notice { + padding-right: 48px; + } +} diff --git a/client/app/assets/less/main.less b/client/app/assets/less/main.less index 8a6ba78c19..fe84375419 100644 --- a/client/app/assets/less/main.less +++ b/client/app/assets/less/main.less @@ -7,7 +7,6 @@ /** Load Vendors Dependencies **/ @import '~font-awesome/less/font-awesome'; @import '~ui-select/dist/select.css'; -@import '~angular-toastr/src/toastr'; @import '~angular-resizable/src/angular-resizable.css'; @import '~material-design-iconic-font/dist/css/material-design-iconic-font.css'; @import '~pace-progress/themes/blue/pace-theme-minimal.css'; diff --git a/client/app/components/QueryEditor.jsx b/client/app/components/QueryEditor.jsx index 0b469cb36e..ff7b059caa 100644 --- a/client/app/components/QueryEditor.jsx +++ b/client/app/components/QueryEditor.jsx @@ -5,7 +5,7 @@ import { react2angular } from 'react2angular'; import AceEditor from 'react-ace'; import ace from 'brace'; -import toastr from 'angular-toastr'; +import notification from '@/services/notification'; import 'brace/ext/language_tools'; import 'brace/mode/json'; @@ -209,7 +209,7 @@ class QueryEditor extends React.Component { formatQuery = () => { Query.format(this.props.dataSource.syntax || 'sql', this.props.queryText) .then(this.updateQuery) - .catch(error => toastr.error(error)); + .catch(error => notification.error(error)); }; toggleAutocomplete = (state) => { diff --git a/client/app/components/QuerySelector.jsx b/client/app/components/QuerySelector.jsx index b95f39a5d8..979ce63a61 100644 --- a/client/app/components/QuerySelector.jsx +++ b/client/app/components/QuerySelector.jsx @@ -6,7 +6,7 @@ import { debounce, find } from 'lodash'; import Input from 'antd/lib/input'; import Select from 'antd/lib/select'; import { Query } from '@/services/query'; -import { toastr } from '@/services/ng'; +import notification from '@/services/notification'; import { QueryTagsControl } from '@/components/tags-control/TagsControl'; const SEARCH_DEBOUNCE_DURATION = 200; @@ -94,7 +94,7 @@ export function QuerySelector(props) { if (queryId) { query = find(searchResults, { id: queryId }); if (!query) { // shouldn't happen - toastr.error('Something went wrong... Couldn\'t select query'); + notification.error('Something went wrong...', 'Couldn\'t select query'); } } diff --git a/client/app/components/SelectItemsDialog.jsx b/client/app/components/SelectItemsDialog.jsx index 359198acf8..df4be5e109 100644 --- a/client/app/components/SelectItemsDialog.jsx +++ b/client/app/components/SelectItemsDialog.jsx @@ -9,7 +9,7 @@ import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper'; import { BigMessage } from '@/components/BigMessage'; import LoadingState from '@/components/items-list/components/LoadingState'; -import { toastr } from '@/services/ng'; +import notification from '@/services/notification'; class SelectItemsDialog extends React.Component { static propTypes = { @@ -100,7 +100,7 @@ class SelectItemsDialog extends React.Component { }) .catch(() => { this.setState({ saveInProgress: false }); - toastr.error('Failed to save some of selected items.'); + notification.error('Failed to save some of selected items.'); }); }); } diff --git a/client/app/components/alerts/alert-subscriptions/index.js b/client/app/components/alerts/alert-subscriptions/index.js index efc7c587d2..2c1bf332d6 100644 --- a/client/app/components/alerts/alert-subscriptions/index.js +++ b/client/app/components/alerts/alert-subscriptions/index.js @@ -1,7 +1,8 @@ import { includes, without, compact } from 'lodash'; +import notification from '@/services/notification'; import template from './alert-subscriptions.html'; -function controller($scope, $q, $sce, currentUser, AlertSubscription, Destination, toastr) { +function controller($scope, $q, $sce, currentUser, AlertSubscription, Destination) { 'ngInject'; $scope.newSubscription = {}; @@ -60,7 +61,7 @@ function controller($scope, $q, $sce, currentUser, AlertSubscription, Destinatio sub.$save( () => { - toastr.success('Subscribed.'); + notification.success('Subscribed.'); $scope.subscribers.push(sub); $scope.destinations = without($scope.destinations, $scope.newSubscription.destination); if ($scope.destinations.length > 0) { @@ -70,7 +71,7 @@ function controller($scope, $q, $sce, currentUser, AlertSubscription, Destinatio } }, () => { - toastr.error('Failed saving subscription.'); + notification.error('Failed saving subscription.'); }, ); }; @@ -81,7 +82,7 @@ function controller($scope, $q, $sce, currentUser, AlertSubscription, Destinatio subscriber.$delete( () => { - toastr.success('Unsubscribed'); + notification.success('Unsubscribed'); $scope.subscribers = without($scope.subscribers, subscriber); if (destination) { $scope.destinations.push(destination); @@ -94,7 +95,7 @@ function controller($scope, $q, $sce, currentUser, AlertSubscription, Destinatio } }, () => { - toastr.error('Failed unsubscribing.'); + notification.error('Failed unsubscribing.'); }, ); }; diff --git a/client/app/components/dashboards/AddTextboxDialog.jsx b/client/app/components/dashboards/AddTextboxDialog.jsx index c7424293c1..b5d47ca999 100644 --- a/client/app/components/dashboards/AddTextboxDialog.jsx +++ b/client/app/components/dashboards/AddTextboxDialog.jsx @@ -7,7 +7,7 @@ import Input from 'antd/lib/input'; import Tooltip from 'antd/lib/tooltip'; import Divider from 'antd/lib/divider'; import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper'; -import { toastr } from '@/services/ng'; +import notification from '@/services/notification'; import './AddTextboxDialog.less'; @@ -44,7 +44,7 @@ class AddTextboxDialog extends React.Component { this.props.dialog.close(); }) .catch(() => { - toastr.error('Widget could not be added'); + notification.error('Widget could not be added'); }) .finally(() => { this.setState({ saveInProgress: false }); diff --git a/client/app/components/dashboards/AddWidgetDialog.jsx b/client/app/components/dashboards/AddWidgetDialog.jsx index d4dc20427f..12812d451f 100644 --- a/client/app/components/dashboards/AddWidgetDialog.jsx +++ b/client/app/components/dashboards/AddWidgetDialog.jsx @@ -10,7 +10,7 @@ import { } from '@/components/ParameterMappingInput'; import { QuerySelector } from '@/components/QuerySelector'; -import { toastr } from '@/services/ng'; +import notification from '@/services/notification'; import { Query } from '@/services/query'; @@ -84,7 +84,7 @@ class AddWidgetDialog extends React.Component { this.props.dialog.close(); }) .catch(() => { - toastr.error('Widget could not be added'); + notification.error('Widget could not be added'); }) .finally(() => { this.setState({ saveInProgress: false }); diff --git a/client/app/components/dashboards/EditParameterMappingsDialog.jsx b/client/app/components/dashboards/EditParameterMappingsDialog.jsx index b26d01baa3..e7cc8c7537 100644 --- a/client/app/components/dashboards/EditParameterMappingsDialog.jsx +++ b/client/app/components/dashboards/EditParameterMappingsDialog.jsx @@ -10,6 +10,7 @@ import { editableMappingsToParameterMappings, synchronizeWidgetTitles, } from '@/components/ParameterMappingInput'; +import notification from '@/services/notification'; export function getParamValuesSnapshot(mappings, dashboardParameters) { return map( @@ -61,7 +62,6 @@ class EditParameterMappingsDialog extends React.Component { } saveWidget() { - const toastr = this.props.toastr; // eslint-disable-line react/prop-types const widget = this.props.widget; this.setState({ saveInProgress: true }); @@ -84,7 +84,7 @@ class EditParameterMappingsDialog extends React.Component { this.props.dialog.close(valuesChanged); }) .catch(() => { - toastr.error('Widget cannot be updated'); + notification.error('Widget cannot be updated'); }) .finally(() => { this.setState({ saveInProgress: false }); diff --git a/client/app/components/dashboards/widget.js b/client/app/components/dashboards/widget.js index b200ce15d6..0f2d83dc13 100644 --- a/client/app/components/dashboards/widget.js +++ b/client/app/components/dashboards/widget.js @@ -3,6 +3,7 @@ import template from './widget.html'; import editTextBoxTemplate from './edit-text-box.html'; import widgetDialogTemplate from './widget-dialog.html'; import EditParameterMappingsDialog from '@/components/dashboards/EditParameterMappingsDialog'; +import notification from '@/services/notification'; import './widget.less'; import './widget-dialog.less'; @@ -25,7 +26,7 @@ const EditTextBoxComponent = { close: '&', dismiss: '&', }, - controller(toastr) { + controller() { 'ngInject'; this.saveInProgress = false; @@ -40,7 +41,7 @@ const EditTextBoxComponent = { this.close(); }) .catch(() => { - toastr.error('Widget can not be updated'); + notification.error('Widget can not be updated'); }) .finally(() => { this.saveInProgress = false; diff --git a/client/app/components/dynamic-form/DynamicForm.jsx b/client/app/components/dynamic-form/DynamicForm.jsx index 952eba940f..b4be5c407e 100644 --- a/client/app/components/dynamic-form/DynamicForm.jsx +++ b/client/app/components/dynamic-form/DynamicForm.jsx @@ -7,9 +7,9 @@ import Checkbox from 'antd/lib/checkbox'; import Button from 'antd/lib/button'; import Upload from 'antd/lib/upload'; import Icon from 'antd/lib/icon'; +import notification from '@/services/notification'; import { includes } from 'lodash'; import { react2angular } from 'react2angular'; -import { toastr } from '@/services/ng'; import { Field, Action, AntdForm } from '../proptypes'; import helper from './dynamicFormHelper'; @@ -83,11 +83,11 @@ export const DynamicForm = Form.create()(class DynamicForm extends React.Compone const { setFieldsValue, getFieldsValue } = this.props.form; this.setState({ isSubmitting: false }); setFieldsValue(getFieldsValue()); // reset form touched state - toastr.success(msg); + notification.success(msg); }, (msg) => { this.setState({ isSubmitting: false }); - toastr.error(msg); + notification.error(msg); }, ); } else this.setState({ isSubmitting: false }); diff --git a/client/app/components/groups/DeleteGroupButton.jsx b/client/app/components/groups/DeleteGroupButton.jsx index 021a84e30a..0e2b5a694c 100644 --- a/client/app/components/groups/DeleteGroupButton.jsx +++ b/client/app/components/groups/DeleteGroupButton.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import Button from 'antd/lib/button'; import Modal from 'antd/lib/modal'; import Tooltip from 'antd/lib/tooltip'; -import { toastr } from '@/services/ng'; +import notification from '@/services/notification'; function deleteGroup(event, group, onGroupDeleted) { Modal.confirm({ @@ -15,7 +15,7 @@ function deleteGroup(event, group, onGroupDeleted) { cancelText: 'No', onOk: () => { group.$delete(() => { - toastr.success('Group deleted successfully.'); + notification.success('Group deleted successfully.'); onGroupDeleted(); }); }, diff --git a/client/app/components/permissions-editor/index.js b/client/app/components/permissions-editor/index.js index 9ebe88f92d..d0a8a7631c 100644 --- a/client/app/components/permissions-editor/index.js +++ b/client/app/components/permissions-editor/index.js @@ -1,4 +1,5 @@ import { includes, each, filter } from 'lodash'; +import notification from '@/services/notification'; import template from './permissions-editor.html'; const PermissionsEditorComponent = { @@ -8,7 +9,7 @@ const PermissionsEditorComponent = { close: '&', dismiss: '&', }, - controller($http, User, toastr) { + controller($http, User) { 'ngInject'; this.grantees = []; @@ -58,9 +59,9 @@ const PermissionsEditorComponent = { loadGrantees(); }).catch((error) => { if (error.status === 403) { - toastr.error('You cannot add a user to this dashboard. Ask the dashboard owner to grant them permissions.'); + notification.error('You cannot add a user to this dashboard.', 'Ask the dashboard owner to grant them permissions.'); } else { - toastr.error('Something went wrong.'); + notification.error('Something went wrong.'); } }); }; diff --git a/client/app/components/users/ChangePasswordDialog.jsx b/client/app/components/users/ChangePasswordDialog.jsx index eb0b652462..600724859a 100644 --- a/client/app/components/users/ChangePasswordDialog.jsx +++ b/client/app/components/users/ChangePasswordDialog.jsx @@ -4,7 +4,7 @@ import Modal from 'antd/lib/modal'; import Input from 'antd/lib/input'; import { isFunction } from 'lodash'; import { User } from '@/services/user'; -import { toastr } from '@/services/ng'; +import notification from '@/services/notification'; import { UserProfile } from '../proptypes'; import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper'; @@ -68,10 +68,10 @@ class ChangePasswordDialog extends React.Component { this.setState({ updatingPassword: true }); User.save(userData, () => { - toastr.success('Saved.'); + notification.success('Saved.'); this.props.dialog.close({ success: true }); }, (error = {}) => { - toastr.error(error.data && error.data.message || 'Failed saving.'); + notification.error(error.data && error.data.message || 'Failed saving.'); this.setState({ updatingPassword: false }); }); } else { diff --git a/client/app/config/index.js b/client/app/config/index.js index fe3a83def5..d886abfe4e 100644 --- a/client/app/config/index.js +++ b/client/app/config/index.js @@ -13,7 +13,6 @@ import ngResource from 'angular-resource'; import uiBootstrap from 'angular-ui-bootstrap'; import uiSelect from 'ui-select'; import ngMessages from 'angular-messages'; -import toastr from 'angular-toastr'; import ngUpload from 'angular-base64-upload'; import vsRepeat from 'angular-vs-repeat'; import 'brace'; @@ -49,7 +48,6 @@ const requirements = [ uiBootstrap, ngMessages, uiSelect, - toastr, 'ui.ace', ngUpload, 'angularResizable', diff --git a/client/app/index.js b/client/app/index.js index 5b24c90750..32b0c2107f 100644 --- a/client/app/index.js +++ b/client/app/index.js @@ -1,15 +1,10 @@ import ngModule from '@/config'; -ngModule.config(($locationProvider, $compileProvider, uiSelectConfig, toastrConfig) => { +ngModule.config(($locationProvider, $compileProvider, uiSelectConfig) => { $compileProvider.debugInfoEnabled(false); $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|data|tel|sms|mailto):/); $locationProvider.html5Mode(true); uiSelectConfig.theme = 'bootstrap'; - - Object.assign(toastrConfig, { - positionClass: 'toast-bottom-right', - timeOut: 2000, - }); }); // Update ui-select's template to use Font-Awesome instead of glyphicon. diff --git a/client/app/pages/alert/index.js b/client/app/pages/alert/index.js index 4020c39b4a..0d402d9e32 100644 --- a/client/app/pages/alert/index.js +++ b/client/app/pages/alert/index.js @@ -1,7 +1,8 @@ import { template as templateBuilder } from 'lodash'; +import notification from '@/services/notification'; import template from './alert.html'; -function AlertCtrl($scope, $routeParams, $location, $sce, toastr, currentUser, Query, Events, Alert) { +function AlertCtrl($scope, $routeParams, $location, $sce, currentUser, Query, Events, Alert) { this.alertId = $routeParams.alertId; if (this.alertId === 'new') { @@ -61,13 +62,13 @@ function AlertCtrl($scope, $routeParams, $location, $sce, toastr, currentUser, Q } this.alert.$save( (alert) => { - toastr.success('Saved.'); + notification.success('Saved.'); if (this.alertId === 'new') { $location.path(`/alerts/${alert.id}`).replace(); } }, () => { - toastr.error('Failed saving alert.'); + notification.error('Failed saving alert.'); }, ); }; @@ -76,10 +77,10 @@ function AlertCtrl($scope, $routeParams, $location, $sce, toastr, currentUser, Q this.alert.$delete( () => { $location.path('/alerts'); - toastr.success('Alert deleted.'); + notification.success('Alert deleted.'); }, () => { - toastr.error('Failed deleting alert.'); + notification.error('Failed deleting alert.'); }, ); }; diff --git a/client/app/pages/dashboards/ShareDashboardDialog.jsx b/client/app/pages/dashboards/ShareDashboardDialog.jsx index 6dac6862b8..7d1b8cb1d5 100644 --- a/client/app/pages/dashboards/ShareDashboardDialog.jsx +++ b/client/app/pages/dashboards/ShareDashboardDialog.jsx @@ -5,7 +5,8 @@ import Switch from 'antd/lib/switch'; import Modal from 'antd/lib/modal'; import Form from 'antd/lib/form'; import Alert from 'antd/lib/alert'; -import { $http, toastr } from '@/services/ng'; +import { $http } from '@/services/ng'; +import notification from '@/services/notification'; import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper'; import InputWithCopy from '@/components/InputWithCopy'; import { HelpTrigger } from '@/components/HelpTrigger'; @@ -60,7 +61,7 @@ class ShareDashboardDialog extends React.Component { dashboard.public_url = data.public_url; }) .error(() => { - toastr.error('Failed to turn on sharing for this dashboard'); + notification.error('Failed to turn on sharing for this dashboard'); }) .finally(() => { this.setState({ saving: false }); @@ -78,7 +79,7 @@ class ShareDashboardDialog extends React.Component { delete dashboard.public_url; }) .error(() => { - toastr.error('Failed to turn off sharing for this dashboard'); + notification.error('Failed to turn off sharing for this dashboard'); }) .finally(() => { this.setState({ saving: false }); diff --git a/client/app/pages/dashboards/dashboard.js b/client/app/pages/dashboards/dashboard.js index 6069ed9e78..1335ddc5a3 100644 --- a/client/app/pages/dashboards/dashboard.js +++ b/client/app/pages/dashboards/dashboard.js @@ -12,6 +12,7 @@ import template from './dashboard.html'; import ShareDashboardDialog from './ShareDashboardDialog'; import AddWidgetDialog from '@/components/dashboards/AddWidgetDialog'; import AddTextboxDialog from '@/components/dashboards/AddTextboxDialog'; +import notification from '@/services/notification'; import './dashboard.less'; @@ -44,7 +45,6 @@ function DashboardCtrl( currentUser, clientConfig, Events, - toastr, ) { this.saveInProgress = false; @@ -59,7 +59,7 @@ function DashboardCtrl( .all(_.map(widgets, widget => widget.save())) .then(() => { if (showMessages) { - toastr.success('Changes saved.'); + notification.success('Changes saved.'); } // Update original widgets positions _.each(widgets, (widget) => { @@ -68,7 +68,7 @@ function DashboardCtrl( }) .catch(() => { if (showMessages) { - toastr.error('Error saving changes.'); + notification.error('Error saving changes.'); } }) .finally(() => { @@ -281,12 +281,12 @@ function DashboardCtrl( }, (error) => { if (error.status === 403) { - toastr.error('Dashboard update failed: Permission Denied.'); + notification.error('Dashboard update failed', 'Permission Denied.'); } else if (error.status === 409) { - toastr.error( - 'It seems like the dashboard has been modified by another user. ' + + notification.error( + 'It seems like the dashboard has been modified by another user. ', 'Please copy/backup your changes and reload this page.', - { autoDismiss: false }, + { duration: null }, ); } }, @@ -314,12 +314,12 @@ function DashboardCtrl( }, (error) => { if (error.status === 403) { - toastr.error('Name update failed: Permission denied.'); + notification.error('Name update failed', 'Permission denied.'); } else if (error.status === 409) { - toastr.error( - 'It seems like the dashboard has been modified by another user. ' + - 'Please copy/backup your changes and reload this page.', - { autoDismiss: false }, + notification.error( + 'It seems like the dashboard has been modified by another user. ', + 'Please copy/backup your changes and reload this page.', + { duration: null }, ); } }, diff --git a/client/app/pages/data-sources/show.js b/client/app/pages/data-sources/show.js index f6754f8bdd..efd96c2510 100644 --- a/client/app/pages/data-sources/show.js +++ b/client/app/pages/data-sources/show.js @@ -1,20 +1,21 @@ import { find } from 'lodash'; import debug from 'debug'; import template from './show.html'; +import notification from '@/services/notification'; const logger = debug('redash:http'); export const deleteConfirm = { class: 'btn-warning', title: 'Delete' }; -export function logAndToastrError(deleteObject, httpResponse, toastr) { +export function logAndNotifyError(deleteObject, httpResponse) { logger('Failed to delete ' + deleteObject + ': ', httpResponse.status, httpResponse.statusText, httpResponse.data); - toastr.error('Failed to delete ' + deleteObject + '.'); + notification.error('Failed to delete ' + deleteObject + '.'); } -export function toastrSuccessAndPath(deleteObject, deletePath, toastr, $location) { - toastr.success(deleteObject + ' deleted successfully.'); +export function notifySuccessAndPath(deleteObject, deletePath, $location) { + notification.success(deleteObject + ' deleted successfully.'); $location.path('/' + deletePath + '/'); } function DataSourceCtrl( - $scope, $route, $routeParams, $http, $location, toastr, + $scope, $route, $routeParams, $http, $location, currentUser, AlertDialog, DataSource, ) { $scope.dataSource = $route.current.locals.dataSource; @@ -53,9 +54,9 @@ function DataSourceCtrl( function deleteDataSource(callback) { const doDelete = () => { $scope.dataSource.$delete(() => { - toastrSuccessAndPath('Data source', 'data_sources', toastr, $location); + notifySuccessAndPath('Data source', 'data_sources', $location); }, (httpResponse) => { - logAndToastrError('data source', httpResponse, toastr); + logAndNotifyError('data source', httpResponse); }); }; @@ -68,14 +69,14 @@ function DataSourceCtrl( function testConnection(callback) { DataSource.test({ id: $scope.dataSource.id }, (httpResponse) => { if (httpResponse.ok) { - toastr.success('Success'); + notification.success('Success'); } else { - toastr.error(httpResponse.message, 'Connection Test Failed:', { timeOut: 10000 }); + notification.error('Connection Test Failed:', httpResponse.message, { duration: 10 }); } callback(); }, (httpResponse) => { logger('Failed to test data source: ', httpResponse.status, httpResponse.statusText, httpResponse); - toastr.error('Unknown error occurred while performing connection test. Please try again later.', 'Connection Test Failed:', { timeOut: 10000 }); + notification.error('Connection Test Failed:', 'Unknown error occurred while performing connection test. Please try again later.', { duration: 10 }); callback(); }); } diff --git a/client/app/pages/destinations/list.js b/client/app/pages/destinations/list.js index 9d907bca24..bcb6ec2b5c 100644 --- a/client/app/pages/destinations/list.js +++ b/client/app/pages/destinations/list.js @@ -1,7 +1,7 @@ import settingsMenu from '@/services/settingsMenu'; import template from './list.html'; -function DestinationsCtrl($scope, $location, toastr, currentUser, Destination) { +function DestinationsCtrl($scope, $location, currentUser, Destination) { $scope.destinations = Destination.query(); } diff --git a/client/app/pages/destinations/show.js b/client/app/pages/destinations/show.js index d8a0987a92..699052b4a6 100644 --- a/client/app/pages/destinations/show.js +++ b/client/app/pages/destinations/show.js @@ -1,9 +1,9 @@ import { find } from 'lodash'; import template from './show.html'; -import { deleteConfirm, logAndToastrError, toastrSuccessAndPath } from '../data-sources/show'; +import { deleteConfirm, logAndNotifyError, notifySuccessAndPath } from '../data-sources/show'; function DestinationCtrl( - $scope, $route, $routeParams, $http, $location, toastr, + $scope, $route, $routeParams, $http, $location, currentUser, AlertDialog, Destination, ) { $scope.destination = $route.current.locals.destination; @@ -31,9 +31,9 @@ function DestinationCtrl( function deleteDestination(callback) { const doDelete = () => { $scope.destination.$delete(() => { - toastrSuccessAndPath('Destination', 'destinations', toastr, $location); + notifySuccessAndPath('Destination', 'destinations', $location); }, (httpResponse) => { - logAndToastrError('destination', httpResponse, toastr); + logAndNotifyError('destination', httpResponse); }); }; diff --git a/client/app/pages/groups/GroupDataSources.jsx b/client/app/pages/groups/GroupDataSources.jsx index 9c7688d272..12abc166ea 100644 --- a/client/app/pages/groups/GroupDataSources.jsx +++ b/client/app/pages/groups/GroupDataSources.jsx @@ -22,7 +22,7 @@ import ListItemAddon from '@/components/groups/ListItemAddon'; import Sidebar from '@/components/groups/DetailsPageSidebar'; import Layout from '@/components/layouts/ContentWithSidebar'; -import { toastr } from '@/services/ng'; +import notification from '@/services/notification'; import { currentUser } from '@/services/auth'; import { Group } from '@/services/group'; import { DataSource } from '@/services/data-source'; @@ -107,7 +107,7 @@ class GroupDataSources extends React.Component { this.props.controller.update(); }) .catch(() => { - toastr.error('Failed to remove data source from group.'); + notification.error('Failed to remove data source from group.'); }); }; @@ -120,7 +120,7 @@ class GroupDataSources extends React.Component { this.forceUpdate(); }) .catch(() => { - toastr.error('Failed change data source permissions.'); + notification.error('Failed change data source permissions.'); }); }; diff --git a/client/app/pages/groups/GroupMembers.jsx b/client/app/pages/groups/GroupMembers.jsx index f591d3b113..b7c73e6102 100644 --- a/client/app/pages/groups/GroupMembers.jsx +++ b/client/app/pages/groups/GroupMembers.jsx @@ -19,7 +19,7 @@ import ListItemAddon from '@/components/groups/ListItemAddon'; import Sidebar from '@/components/groups/DetailsPageSidebar'; import Layout from '@/components/layouts/ContentWithSidebar'; -import { toastr } from '@/services/ng'; +import notification from '@/services/notification'; import { currentUser } from '@/services/auth'; import { Group } from '@/services/group'; import { User } from '@/services/user'; @@ -90,7 +90,7 @@ class GroupMembers extends React.Component { this.props.controller.update(); }) .catch(() => { - toastr.error('Failed to remove member from group.'); + notification.error('Failed to remove member from group.'); }); addMembers = () => { diff --git a/client/app/pages/home/index.js b/client/app/pages/home/index.js index c8f64caad4..3ef8e9b770 100644 --- a/client/app/pages/home/index.js +++ b/client/app/pages/home/index.js @@ -1,6 +1,7 @@ import template from './home.html'; +import notification from '@/services/notification'; -function HomeCtrl(Events, Dashboard, Query, $http, currentUser, toastr) { +function HomeCtrl(Events, Dashboard, Query, $http, currentUser) { Events.record('view', 'page', 'personal_homepage'); this.noDashboards = false; @@ -19,7 +20,7 @@ function HomeCtrl(Events, Dashboard, Query, $http, currentUser, toastr) { this.verifyEmail = () => { $http.post('verification_email/').success(({ message }) => { - toastr.success(message); + notification.success(message); }); }; } diff --git a/client/app/pages/queries/add-to-dashboard.js b/client/app/pages/queries/add-to-dashboard.js index 8365010385..1b4e887ad8 100644 --- a/client/app/pages/queries/add-to-dashboard.js +++ b/client/app/pages/queries/add-to-dashboard.js @@ -1,7 +1,8 @@ import template from './add-to-dashboard.html'; +import notification from '@/services/notification'; const AddToDashboardForm = { - controller($sce, Dashboard, currentUser, toastr, Widget) { + controller($sce, Dashboard, currentUser, Widget) { 'ngInject'; this.vis = this.resolve.vis; @@ -21,10 +22,10 @@ const AddToDashboardForm = { .save() .then(() => { this.close(); - toastr.success('Widget added to dashboard.'); + notification.success('Widget added to dashboard.'); }) .catch(() => { - toastr.error('Widget not added.'); + notification.error('Widget not added.'); }) .finally(() => { this.saveInProgress = false; diff --git a/client/app/pages/queries/view.js b/client/app/pages/queries/view.js index bd4aab4eb5..0a782445d9 100644 --- a/client/app/pages/queries/view.js +++ b/client/app/pages/queries/view.js @@ -4,6 +4,7 @@ import getTags from '@/services/getTags'; import { policy } from '@/services/policy'; import Notifications from '@/services/notifications'; import ScheduleDialog from '@/components/queries/ScheduleDialog'; +import notification from '@/services/notification'; import template from './query.html'; const DEFAULT_TAB = 'table'; @@ -20,7 +21,6 @@ function QueryViewCtrl( Title, AlertDialog, clientConfig, - toastr, $uibModal, currentUser, Query, @@ -79,9 +79,9 @@ function QueryViewCtrl( } else if (data.error.code === SCHEMA_NOT_SUPPORTED) { $scope.schema = undefined; } else if (data.error.code === SCHEMA_LOAD_ERROR) { - toastr.error('Schema refresh failed. Please try again later.'); + notification.error('Schema refresh failed.', 'Please try again later.'); } else { - toastr.error('Schema refresh failed. Please try again later.'); + notification.error('Schema refresh failed.', 'Please try again later.'); } }); } @@ -253,7 +253,7 @@ function QueryViewCtrl( return Query.save( request, (updatedQuery) => { - toastr.success(options.successMessage); + notification.success(options.successMessage); $scope.query.version = updatedQuery.version; }, (error) => { @@ -267,13 +267,14 @@ function QueryViewCtrl( AlertDialog.open(title, message, confirm).then(overwrite); } else { - toastr.error( + notification.error( + 'Changes not saved', errorMessage + ' Please copy/backup your changes and reload this page.', - { autoDismiss: false }, + { duration: null }, ); } } else { - toastr.error(options.errorMessage); + notification.error(options.errorMessage); } }, ).$promise; @@ -316,7 +317,7 @@ function QueryViewCtrl( $scope.query.schedule = null; }, () => { - toastr.error('Query could not be archived.'); + notification.error('Query could not be archived.'); }, ); } @@ -377,7 +378,7 @@ function QueryViewCtrl( $scope.query.visualizations = $scope.query.visualizations.filter(v => vis.id !== v.id); }, () => { - toastr.error("Error deleting visualization. Maybe it's used in a dashboard?"); + notification.error('Error deleting visualization.', 'Maybe it\'s used in a dashboard?'); }, ); }); diff --git a/client/app/pages/query-snippets/edit.js b/client/app/pages/query-snippets/edit.js index 897138cc28..9430d52de6 100644 --- a/client/app/pages/query-snippets/edit.js +++ b/client/app/pages/query-snippets/edit.js @@ -1,7 +1,8 @@ import 'brace/mode/snippets'; +import notification from '@/services/notification'; import template from './edit.html'; -function SnippetCtrl($routeParams, $http, $location, toastr, currentUser, AlertDialog, QuerySnippet) { +function SnippetCtrl($routeParams, $http, $location, currentUser, AlertDialog, QuerySnippet) { this.snippetId = $routeParams.snippetId; this.editorOptions = { @@ -20,12 +21,12 @@ function SnippetCtrl($routeParams, $http, $location, toastr, currentUser, AlertD this.saveChanges = () => { this.snippet.$save((snippet) => { - toastr.success('Saved.'); + notification.success('Saved.'); if (this.snippetId === 'new') { $location.path(`/query_snippets/${snippet.id}`).replace(); } }, () => { - toastr.error('Failed saving snippet.'); + notification.error('Failed saving snippet.'); }); }; @@ -33,9 +34,9 @@ function SnippetCtrl($routeParams, $http, $location, toastr, currentUser, AlertD const doDelete = () => { this.snippet.$delete(() => { $location.path('/query_snippets'); - toastr.success('Query snippet deleted.'); + notification.success('Query snippet deleted.'); }, () => { - toastr.error('Failed deleting query snippet.'); + notification.error('Failed deleting query snippet.'); }); }; diff --git a/client/app/pages/settings/organization.js b/client/app/pages/settings/organization.js index e615b6b07a..48a67c4311 100644 --- a/client/app/pages/settings/organization.js +++ b/client/app/pages/settings/organization.js @@ -1,7 +1,8 @@ import settingsMenu from '@/services/settingsMenu'; +import notification from '@/services/notification'; import template from './organization.html'; -function OrganizationSettingsCtrl($http, toastr, clientConfig, Events) { +function OrganizationSettingsCtrl($http, clientConfig, Events) { Events.record('view', 'page', 'org_settings'); this.settings = {}; @@ -12,14 +13,14 @@ function OrganizationSettingsCtrl($http, toastr, clientConfig, Events) { this.update = (key) => { $http.post('api/settings/organization', { [key]: this.settings[key] }).then((response) => { this.settings = response.data.settings; - toastr.success('Settings changes saved.'); + notification.success('Settings changes saved.'); if (this.disablePasswordLoginToggle() && this.settings.auth_password_login_enabled === false) { this.settings.auth_password_login_enabled = true; this.update('auth_password_login_enabled'); } }).catch(() => { - toastr.error('Failed saving changes.'); + notification.error('Failed saving changes.'); }); }; diff --git a/client/app/pages/users/UsersList.jsx b/client/app/pages/users/UsersList.jsx index 1299a8ec89..925215fd40 100644 --- a/client/app/pages/users/UsersList.jsx +++ b/client/app/pages/users/UsersList.jsx @@ -27,7 +27,7 @@ import { currentUser } from '@/services/auth'; import { policy } from '@/services/policy'; import { User } from '@/services/user'; import navigateTo from '@/services/navigateTo'; -import { toastr } from '@/services/ng'; +import notification from '@/services/notification'; import { absoluteUrl } from '@/services/utils'; function UsersListActions({ user, enableUser, disableUser, deleteUser }) { @@ -127,7 +127,7 @@ class UsersList extends React.Component { } createUser = values => User.create(values).$promise.then((user) => { - toastr.success('Saved.'); + notification.success('Saved.'); if (user.invite_link) { Modal.warning({ title: 'Email not sent!', content: ( diff --git a/client/app/services/ng.js b/client/app/services/ng.js index e48cd00e20..f4a9ec61fa 100644 --- a/client/app/services/ng.js +++ b/client/app/services/ng.js @@ -6,7 +6,6 @@ export let $routeParams = null; // eslint-disable-line import/no-mutable-exports export let $q = null; // eslint-disable-line import/no-mutable-exports export let $rootScope = null; // eslint-disable-line import/no-mutable-exports export let $uibModal = null; // eslint-disable-line import/no-mutable-exports -export let toastr = null; // eslint-disable-line import/no-mutable-exports export default function init(ngModule) { ngModule.run(($injector) => { @@ -18,7 +17,6 @@ export default function init(ngModule) { $q = $injector.get('$q'); $rootScope = $injector.get('$rootScope'); $uibModal = $injector.get('$uibModal'); - toastr = $injector.get('toastr'); }); } diff --git a/client/app/services/notification.js b/client/app/services/notification.js new file mode 100644 index 0000000000..eef7cffa46 --- /dev/null +++ b/client/app/services/notification.js @@ -0,0 +1,20 @@ +import notification from 'antd/lib/notification'; + +notification.config({ + placement: 'bottomRight', + duration: 3, +}); + +const simpleNotification = {}; + +['success', 'error', 'info', 'warning', 'warn'].forEach((action) => { + // eslint-disable-next-line arrow-body-style + simpleNotification[action] = (message, description = null, props = null) => { + return notification[action]({ ...props, message, description }); + }; +}); + +export default { // export Ant's notification and replace actions + ...notification, + ...simpleNotification, +}; diff --git a/client/app/services/offline-listener.js b/client/app/services/offline-listener.js index a7ddad91b5..30f42e8738 100644 --- a/client/app/services/offline-listener.js +++ b/client/app/services/offline-listener.js @@ -1,8 +1,8 @@ -import { toastr } from '@/services/ng'; +import notification from '@/services/notification'; -function addOnlineListener(toast) { +function addOnlineListener(notificationKey) { function onlineStateHandler() { - toastr.remove(toast.toastId); + notification.close(notificationKey); window.removeEventListener('online', onlineStateHandler); } window.addEventListener('online', onlineStateHandler); @@ -11,13 +11,11 @@ function addOnlineListener(toast) { export default function init(ngModule) { ngModule.run(() => { window.addEventListener('offline', () => { - const toast = toastr.warning('
Please check your Internet connection.
', '', { - allowHtml: true, - autoDismiss: false, - timeOut: false, - tapToDismiss: true, + notification.warning('Please check your Internet connection.', null, { + key: 'connectionNotification', + duration: null, }); - addOnlineListener(toast); + addOnlineListener('connectionNotification'); }); }); } diff --git a/client/app/services/user.js b/client/app/services/user.js index 63ba12550a..50b21899a5 100644 --- a/client/app/services/user.js +++ b/client/app/services/user.js @@ -1,5 +1,6 @@ -import { isString } from 'lodash'; -import { $http, $sanitize, toastr } from '@/services/ng'; +import { isString, get } from 'lodash'; +import { $http, $sanitize } from '@/services/ng'; +import notification from '@/services/notification'; import { clientConfig } from '@/services/auth'; export let User = null; // eslint-disable-line import/no-mutable-exports @@ -14,17 +15,17 @@ function enableUser(user) { return $http .delete(disableResource(user)) .then((data) => { - toastr.success(`User ${userName} is now enabled.`, { allowHtml: true }); + notification.success(`User ${userName} is now enabled.`); user.is_disabled = false; user.profile_image_url = data.data.profile_image_url; return data; }) .catch((response) => { - let message = response instanceof Error ? response.message : response.statusText; + let message = get(response, 'data.message', response.statusText); if (!isString(message)) { message = 'Unknown error'; } - toastr.error(`Cannot enable user ${userName}
${message}`, { allowHtml: true }); + notification.error('Cannot enable user', message); }); } @@ -33,18 +34,14 @@ function disableUser(user) { return $http .post(disableResource(user)) .then((data) => { - toastr.warning(`User ${userName} is now disabled.`, { allowHtml: true }); + notification.warning(`User ${userName} is now disabled.`); user.is_disabled = true; user.profile_image_url = data.data.profile_image_url; return data; }) .catch((response = {}) => { - const message = - response.data && response.data.message - ? response.data.message - : `Cannot disable user ${userName}
${response.statusText}`; - - toastr.error(message, { allowHtml: true }); + const message = get(response, 'data.message', response.statusText); + notification.error('Cannot disable user', message); }); } @@ -53,16 +50,12 @@ function deleteUser(user) { return $http .delete(`api/users/${user.id}`) .then((data) => { - toastr.warning(`User ${userName} has been deleted.`, { allowHtml: true }); + notification.warning(`User ${userName} has been deleted.`); return data; }) .catch((response = {}) => { - const message = - response.data && response.data.message - ? response.data.message - : `Cannot delete user ${userName}
${response.statusText}`; - - toastr.error(message, { allowHtml: true }); + const message = get(response, 'data.message', response.statusText); + notification.error('Cannot delete user', message); }); } @@ -82,16 +75,12 @@ function regenerateApiKey(user) { return $http .post(`api/users/${user.id}/regenerate_api_key`) .then(({ data }) => { - toastr.success('The API Key has been updated.'); + notification.success('The API Key has been updated.'); return data.api_key; }) .catch((response = {}) => { - const message = - response.data && response.data.message - ? response.data.message - : `Failed regenerating API Key: ${response.statusText}`; - - toastr.error(message); + const message = get(response, 'data.message', response.statusText); + notification.error('Failed regenerating API Key', message); }); } @@ -100,18 +89,14 @@ function sendPasswordReset(user) { .post(`api/users/${user.id}/reset_password`) .then(({ data }) => { if (clientConfig.mailSettingsMissing) { - toastr.warning('The mail server is not configured.'); + notification.warning('The mail server is not configured.'); return data.reset_link; } - toastr.success('Password reset email sent.'); + notification.success('Password reset email sent.'); }) .catch((response = {}) => { - const message = - response.message - ? response.message - : `Failed to send password reset email: ${response.statusText}`; - - toastr.error(message); + const message = get(response, 'data.message', response.statusText); + notification.error('Failed to send password reset email', message); }); } @@ -120,18 +105,15 @@ function resendInvitation(user) { .post(`api/users/${user.id}/invite`) .then(({ data }) => { if (clientConfig.mailSettingsMissing) { - toastr.warning('The mail server is not configured.'); + notification.warning('The mail server is not configured.'); return data.invite_link; } - toastr.success('Invitation sent.'); + notification.success('Invitation sent.'); }) .catch((response = {}) => { - const message = - response.message - ? response.message - : `Failed to resend invitation: ${response.statusText}`; + const message = get(response, 'data.message', response.statusText); - toastr.error(message); + notification.error('Failed to resend invitation', message); }); } diff --git a/client/app/visualizations/edit-visualization-dialog.js b/client/app/visualizations/edit-visualization-dialog.js index c3d61b6b03..918d8600fd 100644 --- a/client/app/visualizations/edit-visualization-dialog.js +++ b/client/app/visualizations/edit-visualization-dialog.js @@ -1,5 +1,6 @@ import { map } from 'lodash'; import { copy } from 'angular'; +import notification from '@/services/notification'; import template from './edit-visualization-dialog.html'; const EditVisualizationDialog = { @@ -9,7 +10,7 @@ const EditVisualizationDialog = { close: '&', dismiss: '&', }, - controller($window, currentUser, Events, Visualization, toastr) { + controller($window, currentUser, Events, Visualization) { 'ngInject'; this.query = this.resolve.query; @@ -58,7 +59,7 @@ const EditVisualizationDialog = { Visualization.save( this.visualization, (result) => { - toastr.success('Visualization saved'); + notification.success('Visualization saved'); const visIds = map(this.query.visualizations, i => i.id); const index = visIds.indexOf(result.id); @@ -74,7 +75,7 @@ const EditVisualizationDialog = { this.close(); }, () => { - toastr.error('Visualization could not be saved'); + notification.error('Visualization could not be saved'); }, ); }; diff --git a/package-lock.json b/package-lock.json index bc402d3cde..b5806d642a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1509,11 +1509,6 @@ "resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.5.11.tgz", "integrity": "sha1-6/s/ND5UP5su8FD7TC6e4EjRdy8=" }, - "angular-toastr": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/angular-toastr/-/angular-toastr-2.1.1.tgz", - "integrity": "sha1-n4NQykghRaRNARp1W4+zYj1gVEw=" - }, "angular-ui-ace": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/angular-ui-ace/-/angular-ui-ace-0.2.3.tgz", diff --git a/package.json b/package.json index 6b9b52f523..3e39176002 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "angular-resource": "~1.5.8", "angular-route": "~1.5.8", "angular-sanitize": "~1.5.8", - "angular-toastr": "^2.1.1", "angular-ui-ace": "^0.2.3", "angular-ui-bootstrap": "^2.5.0", "angular-vs-repeat": "^1.1.7",