mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch '13739/alert-to-react'
This commit is contained in:
28
public/app/core/actions/appNotification.ts
Normal file
28
public/app/core/actions/appNotification.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { AppNotification } from 'app/types/';
|
||||
|
||||
export enum ActionTypes {
|
||||
AddAppNotification = 'ADD_APP_NOTIFICATION',
|
||||
ClearAppNotification = 'CLEAR_APP_NOTIFICATION',
|
||||
}
|
||||
|
||||
interface AddAppNotificationAction {
|
||||
type: ActionTypes.AddAppNotification;
|
||||
payload: AppNotification;
|
||||
}
|
||||
|
||||
interface ClearAppNotificationAction {
|
||||
type: ActionTypes.ClearAppNotification;
|
||||
payload: number;
|
||||
}
|
||||
|
||||
export type Action = AddAppNotificationAction | ClearAppNotificationAction;
|
||||
|
||||
export const clearAppNotification = (appNotificationId: number) => ({
|
||||
type: ActionTypes.ClearAppNotification,
|
||||
payload: appNotificationId,
|
||||
});
|
||||
|
||||
export const notifyApp = (appNotification: AppNotification) => ({
|
||||
type: ActionTypes.AddAppNotification,
|
||||
payload: appNotification,
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { updateLocation } from './location';
|
||||
import { updateNavIndex, UpdateNavIndexAction } from './navModel';
|
||||
import { notifyApp, clearAppNotification } from './appNotification';
|
||||
|
||||
export { updateLocation, updateNavIndex, UpdateNavIndexAction };
|
||||
export { updateLocation, updateNavIndex, UpdateNavIndexAction, notifyApp, clearAppNotification };
|
||||
|
||||
@@ -5,10 +5,12 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
||||
import { SearchResult } from './components/search/SearchResult';
|
||||
import { TagFilter } from './components/TagFilter/TagFilter';
|
||||
import { SideMenu } from './components/sidemenu/SideMenu';
|
||||
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
||||
|
||||
export function registerAngularDirectives() {
|
||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
||||
react2AngularDirective('sidemenu', SideMenu, []);
|
||||
react2AngularDirective('appNotificationsList', AppNotificationList, []);
|
||||
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
||||
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
|
||||
react2AngularDirective('searchResult', SearchResult, []);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { Component } from 'react';
|
||||
import { AppNotification } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
appNotification: AppNotification;
|
||||
onClearNotification: (id) => void;
|
||||
}
|
||||
|
||||
export default class AppNotificationItem extends Component<Props> {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return this.props.appNotification.id !== nextProps.appNotification.id;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { appNotification, onClearNotification } = this.props;
|
||||
setTimeout(() => {
|
||||
onClearNotification(appNotification.id);
|
||||
}, appNotification.timeout);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { appNotification, onClearNotification } = this.props;
|
||||
return (
|
||||
<div className={`alert-${appNotification.severity} alert`}>
|
||||
<div className="alert-icon">
|
||||
<i className={appNotification.icon} />
|
||||
</div>
|
||||
<div className="alert-body">
|
||||
<div className="alert-title">{appNotification.title}</div>
|
||||
<div className="alert-text">{appNotification.text}</div>
|
||||
</div>
|
||||
<button type="button" className="alert-close" onClick={() => onClearNotification(appNotification.id)}>
|
||||
<i className="fa fa fa-remove" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import AppNotificationItem from './AppNotificationItem';
|
||||
import { notifyApp, clearAppNotification } from 'app/core/actions';
|
||||
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
|
||||
import { AppNotification, StoreState } from 'app/types';
|
||||
import {
|
||||
createErrorNotification,
|
||||
createSuccessNotification,
|
||||
createWarningNotification,
|
||||
} from '../../copy/appNotification';
|
||||
|
||||
export interface Props {
|
||||
appNotifications: AppNotification[];
|
||||
notifyApp: typeof notifyApp;
|
||||
clearAppNotification: typeof clearAppNotification;
|
||||
}
|
||||
|
||||
export class AppNotificationList extends PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
const { notifyApp } = this.props;
|
||||
|
||||
appEvents.on('alert-warning', options => notifyApp(createWarningNotification(options[0], options[1])));
|
||||
appEvents.on('alert-success', options => notifyApp(createSuccessNotification(options[0], options[1])));
|
||||
appEvents.on('alert-error', options => notifyApp(createErrorNotification(options[0], options[1])));
|
||||
}
|
||||
|
||||
onClearAppNotification = id => {
|
||||
this.props.clearAppNotification(id);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { appNotifications } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{appNotifications.map((appNotification, index) => {
|
||||
return (
|
||||
<AppNotificationItem
|
||||
key={`${appNotification.id}-${index}`}
|
||||
appNotification={appNotification}
|
||||
onClearNotification={id => this.onClearAppNotification(id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
appNotifications: state.appNotifications.appNotifications,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
notifyApp,
|
||||
clearAppNotification,
|
||||
};
|
||||
|
||||
export default connectWithStore(AppNotificationList, mapStateToProps, mapDispatchToProps);
|
||||
46
public/app/core/copy/appNotification.ts
Normal file
46
public/app/core/copy/appNotification.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types';
|
||||
|
||||
const defaultSuccessNotification: AppNotification = {
|
||||
title: '',
|
||||
text: '',
|
||||
severity: AppNotificationSeverity.Success,
|
||||
icon: 'fa fa-check',
|
||||
timeout: AppNotificationTimeout.Success,
|
||||
};
|
||||
|
||||
const defaultWarningNotification: AppNotification = {
|
||||
title: '',
|
||||
text: '',
|
||||
severity: AppNotificationSeverity.Warning,
|
||||
icon: 'fa fa-exclamation',
|
||||
timeout: AppNotificationTimeout.Warning,
|
||||
};
|
||||
|
||||
const defaultErrorNotification: AppNotification = {
|
||||
title: '',
|
||||
text: '',
|
||||
severity: AppNotificationSeverity.Error,
|
||||
icon: 'fa fa-exclamation-triangle',
|
||||
timeout: AppNotificationTimeout.Error,
|
||||
};
|
||||
|
||||
export const createSuccessNotification = (title: string, text?: string): AppNotification => ({
|
||||
...defaultSuccessNotification,
|
||||
title: title,
|
||||
text: text,
|
||||
id: Date.now(),
|
||||
});
|
||||
|
||||
export const createErrorNotification = (title: string, text?: string): AppNotification => ({
|
||||
...defaultErrorNotification,
|
||||
title: title,
|
||||
text: text,
|
||||
id: Date.now(),
|
||||
});
|
||||
|
||||
export const createWarningNotification = (title: string, text?: string): AppNotification => ({
|
||||
...defaultWarningNotification,
|
||||
title: title,
|
||||
text: text,
|
||||
id: Date.now(),
|
||||
});
|
||||
51
public/app/core/reducers/appNotification.test.ts
Normal file
51
public/app/core/reducers/appNotification.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { appNotificationsReducer } from './appNotification';
|
||||
import { ActionTypes } from '../actions/appNotification';
|
||||
import { AppNotificationSeverity, AppNotificationTimeout } from 'app/types/';
|
||||
|
||||
describe('clear alert', () => {
|
||||
it('should filter alert', () => {
|
||||
const id1 = 1540301236048;
|
||||
const id2 = 1540301248293;
|
||||
|
||||
const initialState = {
|
||||
appNotifications: [
|
||||
{
|
||||
id: id1,
|
||||
severity: AppNotificationSeverity.Success,
|
||||
icon: 'success',
|
||||
title: 'test',
|
||||
text: 'test alert',
|
||||
timeout: AppNotificationTimeout.Success,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
severity: AppNotificationSeverity.Warning,
|
||||
icon: 'warning',
|
||||
title: 'test2',
|
||||
text: 'test alert fail 2',
|
||||
timeout: AppNotificationTimeout.Warning,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = appNotificationsReducer(initialState, {
|
||||
type: ActionTypes.ClearAppNotification,
|
||||
payload: id2,
|
||||
});
|
||||
|
||||
const expectedResult = {
|
||||
appNotifications: [
|
||||
{
|
||||
id: id1,
|
||||
severity: AppNotificationSeverity.Success,
|
||||
icon: 'success',
|
||||
title: 'test',
|
||||
text: 'test alert',
|
||||
timeout: AppNotificationTimeout.Success,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
19
public/app/core/reducers/appNotification.ts
Normal file
19
public/app/core/reducers/appNotification.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { AppNotification, AppNotificationsState } from 'app/types/';
|
||||
import { Action, ActionTypes } from '../actions/appNotification';
|
||||
|
||||
export const initialState: AppNotificationsState = {
|
||||
appNotifications: [] as AppNotification[],
|
||||
};
|
||||
|
||||
export const appNotificationsReducer = (state = initialState, action: Action): AppNotificationsState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.AddAppNotification:
|
||||
return { ...state, appNotifications: state.appNotifications.concat([action.payload]) };
|
||||
case ActionTypes.ClearAppNotification:
|
||||
return {
|
||||
...state,
|
||||
appNotifications: state.appNotifications.filter(appNotification => appNotification.id !== action.payload),
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { navIndexReducer as navIndex } from './navModel';
|
||||
import { locationReducer as location } from './location';
|
||||
import { appNotificationsReducer as appNotifications } from './appNotification';
|
||||
|
||||
export default {
|
||||
navIndex,
|
||||
location,
|
||||
appNotifications,
|
||||
};
|
||||
|
||||
@@ -1,100 +1,12 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export class AlertSrv {
|
||||
list: any[];
|
||||
constructor() {}
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $timeout, private $rootScope) {
|
||||
this.list = [];
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$rootScope.onAppEvent(
|
||||
'alert-error',
|
||||
(e, alert) => {
|
||||
this.set(alert[0], alert[1], 'error', 12000);
|
||||
},
|
||||
this.$rootScope
|
||||
);
|
||||
|
||||
this.$rootScope.onAppEvent(
|
||||
'alert-warning',
|
||||
(e, alert) => {
|
||||
this.set(alert[0], alert[1], 'warning', 5000);
|
||||
},
|
||||
this.$rootScope
|
||||
);
|
||||
|
||||
this.$rootScope.onAppEvent(
|
||||
'alert-success',
|
||||
(e, alert) => {
|
||||
this.set(alert[0], alert[1], 'success', 3000);
|
||||
},
|
||||
this.$rootScope
|
||||
);
|
||||
|
||||
appEvents.on('alert-warning', options => this.set(options[0], options[1], 'warning', 5000));
|
||||
appEvents.on('alert-success', options => this.set(options[0], options[1], 'success', 3000));
|
||||
appEvents.on('alert-error', options => this.set(options[0], options[1], 'error', 7000));
|
||||
}
|
||||
|
||||
getIconForSeverity(severity) {
|
||||
switch (severity) {
|
||||
case 'success':
|
||||
return 'fa fa-check';
|
||||
case 'error':
|
||||
return 'fa fa-exclamation-triangle';
|
||||
default:
|
||||
return 'fa fa-exclamation';
|
||||
}
|
||||
}
|
||||
|
||||
set(title, text, severity, timeout) {
|
||||
if (_.isObject(text)) {
|
||||
console.log('alert error', text);
|
||||
if (text.statusText) {
|
||||
text = `HTTP Error (${text.status}) ${text.statusText}`;
|
||||
}
|
||||
}
|
||||
|
||||
const newAlert = {
|
||||
title: title || '',
|
||||
text: text || '',
|
||||
severity: severity || 'info',
|
||||
icon: this.getIconForSeverity(severity),
|
||||
};
|
||||
|
||||
const newAlertJson = angular.toJson(newAlert);
|
||||
|
||||
// remove same alert if it already exists
|
||||
_.remove(this.list, value => {
|
||||
return angular.toJson(value) === newAlertJson;
|
||||
});
|
||||
|
||||
this.list.push(newAlert);
|
||||
if (timeout > 0) {
|
||||
this.$timeout(() => {
|
||||
this.list = _.without(this.list, newAlert);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
if (!this.$rootScope.$$phase) {
|
||||
this.$rootScope.$digest();
|
||||
}
|
||||
|
||||
return newAlert;
|
||||
}
|
||||
|
||||
clear(alert) {
|
||||
this.list = _.without(this.list, alert);
|
||||
}
|
||||
|
||||
clearAll() {
|
||||
this.list = [];
|
||||
set() {
|
||||
console.log('old depricated alert srv being used');
|
||||
}
|
||||
}
|
||||
|
||||
// this is just added to not break old plugins that might be using it
|
||||
coreModule.service('alertSrv', AlertSrv);
|
||||
|
||||
@@ -9,7 +9,7 @@ export class BackendSrv {
|
||||
private noBackendCache: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $http, private alertSrv, private $q, private $timeout, private contextSrv) {}
|
||||
constructor(private $http, private $q, private $timeout, private contextSrv) {}
|
||||
|
||||
get(url, params?) {
|
||||
return this.request({ method: 'GET', url: url, params: params });
|
||||
@@ -49,14 +49,14 @@ export class BackendSrv {
|
||||
}
|
||||
|
||||
if (err.status === 422) {
|
||||
this.alertSrv.set('Validation failed', data.message, 'warning', 4000);
|
||||
appEvents.emit('alert-warning', ['Validation failed', data.message]);
|
||||
throw data;
|
||||
}
|
||||
|
||||
data.severity = 'error';
|
||||
let severity = 'error';
|
||||
|
||||
if (err.status < 500) {
|
||||
data.severity = 'warning';
|
||||
severity = 'warning';
|
||||
}
|
||||
|
||||
if (data.message) {
|
||||
@@ -66,7 +66,8 @@ export class BackendSrv {
|
||||
description = message;
|
||||
message = 'Error';
|
||||
}
|
||||
this.alertSrv.set(message, description, data.severity, 10000);
|
||||
|
||||
appEvents.emit('alert-' + severity, [message, description]);
|
||||
}
|
||||
|
||||
throw data;
|
||||
@@ -93,7 +94,7 @@ export class BackendSrv {
|
||||
if (options.method !== 'GET') {
|
||||
if (results && results.data.message) {
|
||||
if (options.showSuccessAlert !== false) {
|
||||
this.alertSrv.set(results.data.message, '', 'success', 3000);
|
||||
appEvents.emit('alert-success', [results.data.message]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('backend_srv', () => {
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {});
|
||||
const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {});
|
||||
|
||||
describe('when handling errors', () => {
|
||||
it('should return the http status code', async () => {
|
||||
|
||||
11
public/app/core/utils/connectWithReduxStore.tsx
Normal file
11
public/app/core/utils/connectWithReduxStore.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { store } from '../../store/configureStore';
|
||||
|
||||
export function connectWithStore(WrappedComponent, ...args) {
|
||||
const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
|
||||
|
||||
return props => {
|
||||
return <ConnectedWrappedComponent {...props} store={store} />;
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Tooltip from 'app/core/components/Tooltip/Tooltip';
|
||||
import SlideDown from 'app/core/components/Animations/SlideDown';
|
||||
import { StoreState, FolderInfo } from 'app/types';
|
||||
@@ -13,7 +12,7 @@ import {
|
||||
import PermissionList from 'app/core/components/PermissionList/PermissionList';
|
||||
import AddPermission from 'app/core/components/PermissionList/AddPermission';
|
||||
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
|
||||
import { store } from 'app/store/configureStore';
|
||||
import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
|
||||
|
||||
export interface Props {
|
||||
dashboardId: number;
|
||||
@@ -95,13 +94,6 @@ export class DashboardPermissions extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
function connectWithStore(WrappedComponent, ...args) {
|
||||
const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
|
||||
return props => {
|
||||
return <ConnectedWrappedComponent {...props} store={store} />;
|
||||
};
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
permissions: state.dashboard.permissions,
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ const template = `
|
||||
`;
|
||||
|
||||
/** @ngInject */
|
||||
function uploadDashboardDirective(timer, alertSrv, $location) {
|
||||
function uploadDashboardDirective(timer, $location) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
@@ -59,7 +59,7 @@ function uploadDashboardDirective(timer, alertSrv, $location) {
|
||||
// Something
|
||||
elem[0].addEventListener('change', file_selected, false);
|
||||
} else {
|
||||
alertSrv.set('Oops', 'Sorry, the HTML5 File APIs are not fully supported in this browser.', 'error');
|
||||
appEvents.emit('alert-error', ['Oops', 'The HTML5 File APIs are not fully supported in this browser']);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -17,7 +17,6 @@ export class GrafanaCtrl {
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
$scope,
|
||||
alertSrv,
|
||||
utilSrv,
|
||||
$rootScope,
|
||||
$controller,
|
||||
@@ -41,11 +40,8 @@ export class GrafanaCtrl {
|
||||
$scope._ = _;
|
||||
|
||||
profiler.init(config, $rootScope);
|
||||
alertSrv.init();
|
||||
utilSrv.init();
|
||||
bridgeSrv.init();
|
||||
|
||||
$scope.dashAlerts = alertSrv;
|
||||
};
|
||||
|
||||
$rootScope.colors = colors;
|
||||
|
||||
25
public/app/types/appNotifications.ts
Normal file
25
public/app/types/appNotifications.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export interface AppNotification {
|
||||
id?: number;
|
||||
severity: AppNotificationSeverity;
|
||||
icon: string;
|
||||
title: string;
|
||||
text: string;
|
||||
timeout: AppNotificationTimeout;
|
||||
}
|
||||
|
||||
export enum AppNotificationSeverity {
|
||||
Success = 'success',
|
||||
Warning = 'warning',
|
||||
Error = 'error',
|
||||
Info = 'info',
|
||||
}
|
||||
|
||||
export enum AppNotificationTimeout {
|
||||
Warning = 5000,
|
||||
Success = 3000,
|
||||
Error = 7000,
|
||||
}
|
||||
|
||||
export interface AppNotificationsState {
|
||||
appNotifications: AppNotification[];
|
||||
}
|
||||
@@ -22,6 +22,12 @@ import {
|
||||
} from './series';
|
||||
import { PanelProps } from './panel';
|
||||
import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins';
|
||||
import {
|
||||
AppNotification,
|
||||
AppNotificationSeverity,
|
||||
AppNotificationsState,
|
||||
AppNotificationTimeout,
|
||||
} from './appNotifications';
|
||||
|
||||
export {
|
||||
Team,
|
||||
@@ -70,6 +76,10 @@ export {
|
||||
DataQueryResponse,
|
||||
DataQueryOptions,
|
||||
PluginDashboard,
|
||||
AppNotification,
|
||||
AppNotificationsState,
|
||||
AppNotificationSeverity,
|
||||
AppNotificationTimeout,
|
||||
};
|
||||
|
||||
export interface StoreState {
|
||||
@@ -82,4 +92,5 @@ export interface StoreState {
|
||||
dashboard: DashboardState;
|
||||
dataSources: DataSourcesState;
|
||||
users: UsersState;
|
||||
appNotifications: AppNotificationsState;
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
|
||||
.alert {
|
||||
padding: 1.25rem 2rem 1.25rem 1.5rem;
|
||||
margin-bottom: $line-height-base;
|
||||
margin-bottom: $panel-margin / 2;
|
||||
text-shadow: 0 2px 0 rgba(255, 255, 255, 0.5);
|
||||
background: $alert-error-bg;
|
||||
position: relative;
|
||||
color: $white;
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
|
||||
border-radius: 2px;
|
||||
border-radius: $border-radius;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@@ -200,21 +200,8 @@
|
||||
|
||||
<grafana-app class="grafana-app" ng-cloak>
|
||||
<sidemenu class="sidemenu"></sidemenu>
|
||||
<app-notifications-list class="page-alert-list"></app-notifications-list>
|
||||
|
||||
<div class="page-alert-list">
|
||||
<div ng-repeat='alert in dashAlerts.list' class="alert-{{alert.severity}} alert">
|
||||
<div class="alert-icon">
|
||||
<i class="{{alert.icon}}"></i>
|
||||
</div>
|
||||
<div class="alert-body">
|
||||
<div class="alert-title">{{alert.title}}</div>
|
||||
<div class="alert-text" ng-bind='alert.text'></div>
|
||||
</div>
|
||||
<button type="button" class="alert-close" ng-click="dashAlerts.clear(alert)">
|
||||
<i class="fa fa fa-remove"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-view">
|
||||
<div class="scroll-canvas" page-scrollbar>
|
||||
|
||||
Reference in New Issue
Block a user