Commit ffffcb11 by Sam Padgett Committed by GitHub

Merge pull request #39 from jeff-phillips-18/toast

Add toast notifications component and filters
parents fdd7f38e 80ee3ebc
.toast-action-divider {
color: @gray-light;
}
.toast-notification-message {
font-weight: 700;
margin-right: 5px;
}
...@@ -14,4 +14,5 @@ ...@@ -14,4 +14,5 @@
@import "_forms.less"; @import "_forms.less";
@import "_guided-tour.less"; @import "_guided-tour.less";
@import "_messages.less"; @import "_messages.less";
@import "_notifications.less";
@import "_ui-select.less"; @import "_ui-select.less";
...@@ -204,6 +204,30 @@ hawtioPluginLoader.addModule('openshiftCommonUI'); ...@@ -204,6 +204,30 @@ hawtioPluginLoader.addModule('openshiftCommonUI');
); );
$templateCache.put('src/components/toast-notifications/toast-notifications.html',
"<div class=\"toast-notifications-list-pf\">\n" +
" <div ng-repeat=\"(notificationID, notification) in notifications track by (notificationID + (notification.message || notification.details))\" ng-if=\"!notification.hidden\"\n" +
" ng-mouseenter=\"setHover(notification, true)\" ng-mouseleave=\"setHover(notification, false)\">\n" +
" <div class=\"toast-pf alert {{notification.type | alertStatus}}\" ng-class=\"{'alert-dismissable': !hideCloseButton}\">\n" +
" <button ng-if=\"!hideCloseButton\" type=\"button\" class=\"close\" ng-click=\"close(notification)\">\n" +
" <span class=\"pficon pficon-close\" aria-hidden=\"true\"></span>\n" +
" <span class=\"sr-only\">Close</span>\n" +
" </button>\n" +
" <span class=\"{{notification.type | alertIcon}}\" aria-hidden=\"true\"></span>\n" +
" <span class=\"sr-only\">{{notification.type}}</span>\n" +
" <span class=\"toast-notification-message\" ng-if=\"notification.message\">{{notification.message}}</span>\n" +
" <span ng-if=\"notification.details\">{{notification.details}}</span>\n" +
" <span ng-repeat=\"link in notification.links\">\n" +
" <a ng-if=\"!link.href\" href=\"\" ng-click=\"onClick(notification, link)\" role=\"button\">{{link.label}}</a>\n" +
" <a ng-if=\"link.href\" ng-href=\"{{link.href}}\" ng-attr-target=\"{{link.target}}\">{{link.label}}</a>\n" +
" <span ng-if=\"!$last\" class=\"toast-action-divider\">|</span>\n" +
" </span>\n" +
" </div>\n" +
" </div>\n" +
"</div>\n"
);
$templateCache.put('src/components/truncate-long-text/truncateLongText.html', $templateCache.put('src/components/truncate-long-text/truncateLongText.html',
"<!--\n" + "<!--\n" +
" Do not remove class `truncated-content` (here or below) even though it's not\n" + " Do not remove class `truncated-content` (here or below) even though it's not\n" +
...@@ -246,9 +270,18 @@ angular.module("openshiftCommonUI") ...@@ -246,9 +270,18 @@ angular.module("openshiftCommonUI")
isDialog: '@' isDialog: '@'
}, },
templateUrl: 'src/components/create-project/createProject.html', templateUrl: 'src/components/create-project/createProject.html',
controller: function($scope, $filter, $location, DataService) { controller: function($scope, $filter, $location, DataService, NotificationsService, displayNameFilter) {
if(!($scope.submitButtonLabel)) {
$scope.submitButtonLabel = 'Create';
}
$scope.isDialog = $scope.isDialog === 'true'; $scope.isDialog = $scope.isDialog === 'true';
var showAlert = function(name, alert) {
$scope.alerts[name] = alert;
NotificationsService.addNotification(alert);
};
$scope.createProject = function() { $scope.createProject = function() {
$scope.disableInputs = true; $scope.disableInputs = true;
if ($scope.createProjectForm.$valid) { if ($scope.createProjectForm.$valid) {
...@@ -270,6 +303,10 @@ angular.module("openshiftCommonUI") ...@@ -270,6 +303,10 @@ angular.module("openshiftCommonUI")
} else { } else {
$location.path("project/" + encodeURIComponent(data.metadata.name) + "/create"); $location.path("project/" + encodeURIComponent(data.metadata.name) + "/create");
} }
showAlert('created-project', {
type: "success",
message: "Project \'" + displayNameFilter(data) + "\' was successfully created."
});
}, function(result) { }, function(result) {
$scope.disableInputs = false; $scope.disableInputs = false;
var data = result.data || {}; var data = result.data || {};
...@@ -277,7 +314,7 @@ angular.module("openshiftCommonUI") ...@@ -277,7 +314,7 @@ angular.module("openshiftCommonUI")
$scope.nameTaken = true; $scope.nameTaken = true;
} else { } else {
var msg = data.message || 'An error occurred creating the project.'; var msg = data.message || 'An error occurred creating the project.';
$scope.alerts['error-creating-project'] = {type: 'error', message: msg}; showAlert('error-creating-project', {type: 'error', message: msg});
} }
}); });
} }
...@@ -299,13 +336,13 @@ angular.module("openshiftCommonUI") ...@@ -299,13 +336,13 @@ angular.module("openshiftCommonUI")
;'use strict'; ;'use strict';
angular.module("openshiftCommonUI") angular.module("openshiftCommonUI")
.directive("deleteProject", function ($uibModal, $location, $filter, $q, hashSizeFilter, APIService, DataService, AlertMessageService, Logger) { .directive("deleteProject", function ($uibModal, $location, $filter, $q, hashSizeFilter, APIService, DataService, AlertMessageService, NotificationsService, Logger) {
return { return {
restrict: "E", restrict: "E",
scope: { scope: {
// The name of project to delete // The name of project to delete
projectName: "@", projectName: "@",
// Alerts object for success and error alerts. // Alerts object for using inline notifications for success and error alerts, notifications are also sent to enable toast notification display.
alerts: "=", alerts: "=",
// Optional display name of the project to delete. // Optional display name of the project to delete.
displayName: "@", displayName: "@",
...@@ -340,6 +377,7 @@ angular.module("openshiftCommonUI") ...@@ -340,6 +377,7 @@ angular.module("openshiftCommonUI")
} else { } else {
AlertMessageService.addAlert(alert); AlertMessageService.addAlert(alert);
} }
NotificationsService.addNotification(alert.data);
}; };
var navigateToList = function() { var navigateToList = function() {
...@@ -400,11 +438,13 @@ angular.module("openshiftCommonUI") ...@@ -400,11 +438,13 @@ angular.module("openshiftCommonUI")
}) })
.catch(function(err) { .catch(function(err) {
// called if failure to delete // called if failure to delete
scope.alerts[projectName] = { var alert = {
type: "error", type: "error",
message: _.capitalize(formattedResource) + "\'" + " could not be deleted.", message: _.capitalize(formattedResource) + "\'" + " could not be deleted.",
details: $filter('getErrorDetails')(err) details: $filter('getErrorDetails')(err)
}; };
scope.alerts[projectName] = alert;
NotificationsService.addNotification(alert);
Logger.error(formattedResource + " could not be deleted.", err); Logger.error(formattedResource + " could not be deleted.", err);
}); });
}); });
...@@ -446,7 +486,7 @@ angular.module("openshiftCommonUI") ...@@ -446,7 +486,7 @@ angular.module("openshiftCommonUI")
isDialog: '@' isDialog: '@'
}, },
templateUrl: 'src/components/edit-project/editProject.html', templateUrl: 'src/components/edit-project/editProject.html',
controller: function($scope, $filter, $location, DataService, annotationNameFilter) { controller: function($scope, $filter, $location, DataService, NotificationsService, annotationNameFilter, displayNameFilter) {
if(!($scope.submitButtonLabel)) { if(!($scope.submitButtonLabel)) {
$scope.submitButtonLabel = 'Save'; $scope.submitButtonLabel = 'Save';
} }
...@@ -483,6 +523,11 @@ angular.module("openshiftCommonUI") ...@@ -483,6 +523,11 @@ angular.module("openshiftCommonUI")
return resource; return resource;
}; };
var showAlert = function(alert) {
$scope.alerts["update"] = alert;
NotificationsService.addNotification(alert);
};
$scope.editableFields = editableFields($scope.project); $scope.editableFields = editableFields($scope.project);
$scope.update = function() { $scope.update = function() {
...@@ -495,20 +540,25 @@ angular.module("openshiftCommonUI") ...@@ -495,20 +540,25 @@ angular.module("openshiftCommonUI")
cleanEditableAnnotations(mergeEditable($scope.project, $scope.editableFields)), cleanEditableAnnotations(mergeEditable($scope.project, $scope.editableFields)),
{projectName: $scope.project.name}, {projectName: $scope.project.name},
{errorNotification: false}) {errorNotification: false})
.then(function() { .then(function(project) {
// angular is actually wrapping the redirect action :/ // angular is actually wrapping the redirect action :/
var cb = $scope.redirectAction(); var cb = $scope.redirectAction();
if (cb) { if (cb) {
cb(encodeURIComponent($scope.project.metadata.name)); cb(encodeURIComponent($scope.project.metadata.name));
} }
showAlert({
type: "success",
message: "Project \'" + displayNameFilter(project) + "\' was successfully updated."
});
}, function(result) { }, function(result) {
$scope.disableInputs = false; $scope.disableInputs = false;
$scope.editableFields = editableFields($scope.project); $scope.editableFields = editableFields($scope.project);
$scope.alerts["update"] = { showAlert({
type: "error", type: "error",
message: "An error occurred while updating the project", message: "An error occurred while updating the project",
details: $filter('getErrorDetails')(result) details: $filter('getErrorDetails')(result)
}; });
}); });
} }
}; };
...@@ -543,6 +593,55 @@ angular.module('openshiftCommonUI') ...@@ -543,6 +593,55 @@ angular.module('openshiftCommonUI')
;'use strict'; ;'use strict';
angular.module('openshiftCommonUI') angular.module('openshiftCommonUI')
.directive('toastNotifications', function(NotificationsService, $timeout) {
return {
restrict: 'E',
scope: {},
templateUrl: 'src/components/toast-notifications/toast-notifications.html',
link: function($scope) {
$scope.notifications = NotificationsService.getNotifications();
$scope.close = function(notification) {
notification.hidden = true;
if (_.isFunction(notification.onClose)) {
notification.onClose();
}
};
$scope.onClick = function(notification, link) {
if (_.isFunction(link.onClick)) {
// If onClick() returns true, also hide the alert.
var close = link.onClick();
if (close) {
notification.hidden = true;
}
}
};
$scope.setHover = function(notification, isHover) {
notification.isHover = isHover;
};
$scope.$watch('notifications', function() {
_.each($scope.notifications, function(notification) {
if (NotificationsService.isAutoDismiss(notification) && !notification.hidden) {
if (!notification.timerId) {
notification.timerId = $timeout(function () {
notification.timerId = -1;
if (!notification.isHover) {
notification.hidden = true;
}
}, NotificationsService.dismissDelay);
} else if (notification.timerId === -1 && !notification.isHover) {
notification.hidden = true;
}
}
});
}, true);
}
};
});
;'use strict';
angular.module('openshiftCommonUI')
// Truncates text to a length, adding a tooltip and an ellipsis if truncated. // Truncates text to a length, adding a tooltip and an ellipsis if truncated.
// Different than `text-overflow: ellipsis` because it allows for multiline text. // Different than `text-overflow: ellipsis` because it allows for multiline text.
.directive('truncateLongText', function(truncateFilter) { .directive('truncateLongText', function(truncateFilter) {
...@@ -574,6 +673,51 @@ angular.module('openshiftCommonUI') ...@@ -574,6 +673,51 @@ angular.module('openshiftCommonUI')
}; };
}); });
;'use strict'; ;'use strict';
angular.module('openshiftCommonUI')
.filter("alertStatus", function() {
return function (type) {
var status;
switch(type) {
case 'error':
status = 'alert-danger';
break;
case 'warning':
status = 'alert-warning';
break;
case 'success':
status = 'alert-success';
break;
default:
status = 'alert-info';
}
return status;
};
})
.filter('alertIcon', function() {
return function (type) {
var icon;
switch(type) {
case 'error':
icon = 'pficon pficon-error-circle-o';
break;
case 'warning':
icon = 'pficon pficon-warning-triangle-o';
break;
case 'success':
icon = 'pficon pficon-ok';
break;
default:
icon = 'pficon pficon-info';
}
return icon;
};
});
;'use strict';
/* jshint unused: false */ /* jshint unused: false */
angular.module('openshiftCommonUI') angular.module('openshiftCommonUI')
...@@ -1072,6 +1216,16 @@ angular.module('openshiftCommonUI') ...@@ -1072,6 +1216,16 @@ angular.module('openshiftCommonUI')
return Object.keys(hash).length; return Object.keys(hash).length;
}; };
}) })
// Wraps _.filter. Works with hashes, unlike ngFilter, which only works
// with arrays.
.filter('filterCollection', function() {
return function(collection, predicate) {
if (!collection || !predicate) {
return collection;
}
return _.filter(collection, predicate);
};
})
.filter('generateName', function() { .filter('generateName', function() {
return function(prefix, length) { return function(prefix, length) {
if (!prefix) { if (!prefix) {
...@@ -1083,6 +1237,21 @@ angular.module('openshiftCommonUI') ...@@ -1083,6 +1237,21 @@ angular.module('openshiftCommonUI')
var randomString = Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1); var randomString = Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1);
return prefix + randomString; return prefix + randomString;
}; };
})
.filter("getErrorDetails", function(upperFirstFilter) {
return function(result, capitalize) {
var error = result.data || {};
if (error.message) {
return capitalize ? upperFirstFilter(error.message) : error.message;
}
var status = result.status || error.status;
if (status) {
return "Status: " + status;
}
return "";
};
}); });
;'use strict'; ;'use strict';
...@@ -1218,3 +1387,75 @@ angular.module('openshiftCommonUI').factory('GuidedTourService', function() { ...@@ -1218,3 +1387,75 @@ angular.module('openshiftCommonUI').factory('GuidedTourService', function() {
cancelTour: cancelTour cancelTour: cancelTour
}; };
}); });
;'use strict';
angular.module('openshiftCommonServices').provider('NotificationsService', function() {
this.dismissDelay = 8000;
this.autoDismissTypes = ['info', 'success'];
this.$get = function() {
var notifications = [];
var dismissDelay = this.dismissDelay;
var autoDismissTypes = this.autoDismissTypes;
var notificationHiddenKey = function(notificationID, namespace) {
if (!namespace) {
return 'hide/notification/' + notificationID;
}
return 'hide/notification/' + namespace + '/' + notificationID;
};
var addNotification = function (notification, notificationID, namespace) {
if (notificationID && isNotificationPermanentlyHidden(notificationID, namespace)) {
notification.hidden = true;
}
notifications.push(notification);
};
var getNotifications = function () {
return notifications;
};
var clearNotifications = function () {
_.take(notifications, 0);
};
var isNotificationPermanentlyHidden = function (notificationID, namespace) {
var key = notificationHiddenKey(notificationID, namespace);
return localStorage.getItem(key) === 'true';
};
var permanentlyHideNotification = function (notificationID, namespace) {
var key = notificationHiddenKey(notificationID, namespace);
localStorage.setItem(key, 'true');
};
var isAutoDismiss = function(notification) {
return _.find(autoDismissTypes, function(type) {
return type === notification.type;
});
};
return {
addNotification: addNotification,
getNotifications: getNotifications,
clearNotifications: clearNotifications,
isNotificationPermanentlyHidden: isNotificationPermanentlyHidden,
permanentlyHideNotification: permanentlyHideNotification,
isAutoDismiss: isAutoDismiss,
dismissDelay: dismissDelay,
autoDismissTypes: autoDismissTypes
};
};
this.setDismissDelay = function(delayInMs) {
this.dismissDelay = delayInMs;
};
this.setAutoDismissTypes = function(arrayOfTypes) {
this.autoDismissTypes = arrayOfTypes;
};
});
...@@ -341,6 +341,13 @@ ul.messenger-theme-flat .messenger-message.alert-info .messenger-message-inner:b ...@@ -341,6 +341,13 @@ ul.messenger-theme-flat .messenger-message.alert-info .messenger-message-inner:b
display: inline-block; display: inline-block;
background-color: transparent; background-color: transparent;
} }
.toast-action-divider {
color: #9c9c9c;
}
.toast-notification-message {
font-weight: 700;
margin-right: 5px;
}
/* Fix some styles with the angular-ui-select bootstrap theme that isn't right when combined with all of our styles */ /* Fix some styles with the angular-ui-select bootstrap theme that isn't right when combined with all of our styles */
.ui-select-bootstrap { .ui-select-bootstrap {
/* Fixes ui-select's input box extending to 100% page width if the page has been resized after page-load /* Fixes ui-select's input box extending to 100% page width if the page has been resized after page-load
......
...@@ -375,6 +375,30 @@ hawtioPluginLoader.addModule('openshiftCommonUI'); ...@@ -375,6 +375,30 @@ hawtioPluginLoader.addModule('openshiftCommonUI');
); );
$templateCache.put('src/components/toast-notifications/toast-notifications.html',
"<div class=\"toast-notifications-list-pf\">\n" +
" <div ng-repeat=\"(notificationID, notification) in notifications track by (notificationID + (notification.message || notification.details))\" ng-if=\"!notification.hidden\"\n" +
" ng-mouseenter=\"setHover(notification, true)\" ng-mouseleave=\"setHover(notification, false)\">\n" +
" <div class=\"toast-pf alert {{notification.type | alertStatus}}\" ng-class=\"{'alert-dismissable': !hideCloseButton}\">\n" +
" <button ng-if=\"!hideCloseButton\" type=\"button\" class=\"close\" ng-click=\"close(notification)\">\n" +
" <span class=\"pficon pficon-close\" aria-hidden=\"true\"></span>\n" +
" <span class=\"sr-only\">Close</span>\n" +
" </button>\n" +
" <span class=\"{{notification.type | alertIcon}}\" aria-hidden=\"true\"></span>\n" +
" <span class=\"sr-only\">{{notification.type}}</span>\n" +
" <span class=\"toast-notification-message\" ng-if=\"notification.message\">{{notification.message}}</span>\n" +
" <span ng-if=\"notification.details\">{{notification.details}}</span>\n" +
" <span ng-repeat=\"link in notification.links\">\n" +
" <a ng-if=\"!link.href\" href=\"\" ng-click=\"onClick(notification, link)\" role=\"button\">{{link.label}}</a>\n" +
" <a ng-if=\"link.href\" ng-href=\"{{link.href}}\" ng-attr-target=\"{{link.target}}\">{{link.label}}</a>\n" +
" <span ng-if=\"!$last\" class=\"toast-action-divider\">|</span>\n" +
" </span>\n" +
" </div>\n" +
" </div>\n" +
"</div>\n"
);
$templateCache.put('src/components/truncate-long-text/truncateLongText.html', $templateCache.put('src/components/truncate-long-text/truncateLongText.html',
"<!--\n" + "<!--\n" +
" Do not remove class `truncated-content` (here or below) even though it's not\n" + " Do not remove class `truncated-content` (here or below) even though it's not\n" +
...@@ -417,9 +441,18 @@ angular.module("openshiftCommonUI") ...@@ -417,9 +441,18 @@ angular.module("openshiftCommonUI")
isDialog: '@' isDialog: '@'
}, },
templateUrl: 'src/components/create-project/createProject.html', templateUrl: 'src/components/create-project/createProject.html',
controller: ["$scope", "$filter", "$location", "DataService", function($scope, $filter, $location, DataService) { controller: ["$scope", "$filter", "$location", "DataService", "NotificationsService", "displayNameFilter", function($scope, $filter, $location, DataService, NotificationsService, displayNameFilter) {
if(!($scope.submitButtonLabel)) {
$scope.submitButtonLabel = 'Create';
}
$scope.isDialog = $scope.isDialog === 'true'; $scope.isDialog = $scope.isDialog === 'true';
var showAlert = function(name, alert) {
$scope.alerts[name] = alert;
NotificationsService.addNotification(alert);
};
$scope.createProject = function() { $scope.createProject = function() {
$scope.disableInputs = true; $scope.disableInputs = true;
if ($scope.createProjectForm.$valid) { if ($scope.createProjectForm.$valid) {
...@@ -441,6 +474,10 @@ angular.module("openshiftCommonUI") ...@@ -441,6 +474,10 @@ angular.module("openshiftCommonUI")
} else { } else {
$location.path("project/" + encodeURIComponent(data.metadata.name) + "/create"); $location.path("project/" + encodeURIComponent(data.metadata.name) + "/create");
} }
showAlert('created-project', {
type: "success",
message: "Project \'" + displayNameFilter(data) + "\' was successfully created."
});
}, function(result) { }, function(result) {
$scope.disableInputs = false; $scope.disableInputs = false;
var data = result.data || {}; var data = result.data || {};
...@@ -448,7 +485,7 @@ angular.module("openshiftCommonUI") ...@@ -448,7 +485,7 @@ angular.module("openshiftCommonUI")
$scope.nameTaken = true; $scope.nameTaken = true;
} else { } else {
var msg = data.message || 'An error occurred creating the project.'; var msg = data.message || 'An error occurred creating the project.';
$scope.alerts['error-creating-project'] = {type: 'error', message: msg}; showAlert('error-creating-project', {type: 'error', message: msg});
} }
}); });
} }
...@@ -470,13 +507,13 @@ angular.module("openshiftCommonUI") ...@@ -470,13 +507,13 @@ angular.module("openshiftCommonUI")
;'use strict'; ;'use strict';
angular.module("openshiftCommonUI") angular.module("openshiftCommonUI")
.directive("deleteProject", ["$uibModal", "$location", "$filter", "$q", "hashSizeFilter", "APIService", "DataService", "AlertMessageService", "Logger", function ($uibModal, $location, $filter, $q, hashSizeFilter, APIService, DataService, AlertMessageService, Logger) { .directive("deleteProject", ["$uibModal", "$location", "$filter", "$q", "hashSizeFilter", "APIService", "DataService", "AlertMessageService", "NotificationsService", "Logger", function ($uibModal, $location, $filter, $q, hashSizeFilter, APIService, DataService, AlertMessageService, NotificationsService, Logger) {
return { return {
restrict: "E", restrict: "E",
scope: { scope: {
// The name of project to delete // The name of project to delete
projectName: "@", projectName: "@",
// Alerts object for success and error alerts. // Alerts object for using inline notifications for success and error alerts, notifications are also sent to enable toast notification display.
alerts: "=", alerts: "=",
// Optional display name of the project to delete. // Optional display name of the project to delete.
displayName: "@", displayName: "@",
...@@ -511,6 +548,7 @@ angular.module("openshiftCommonUI") ...@@ -511,6 +548,7 @@ angular.module("openshiftCommonUI")
} else { } else {
AlertMessageService.addAlert(alert); AlertMessageService.addAlert(alert);
} }
NotificationsService.addNotification(alert.data);
}; };
var navigateToList = function() { var navigateToList = function() {
...@@ -571,11 +609,13 @@ angular.module("openshiftCommonUI") ...@@ -571,11 +609,13 @@ angular.module("openshiftCommonUI")
}) })
.catch(function(err) { .catch(function(err) {
// called if failure to delete // called if failure to delete
scope.alerts[projectName] = { var alert = {
type: "error", type: "error",
message: _.capitalize(formattedResource) + "\'" + " could not be deleted.", message: _.capitalize(formattedResource) + "\'" + " could not be deleted.",
details: $filter('getErrorDetails')(err) details: $filter('getErrorDetails')(err)
}; };
scope.alerts[projectName] = alert;
NotificationsService.addNotification(alert);
Logger.error(formattedResource + " could not be deleted.", err); Logger.error(formattedResource + " could not be deleted.", err);
}); });
}); });
...@@ -617,7 +657,7 @@ angular.module("openshiftCommonUI") ...@@ -617,7 +657,7 @@ angular.module("openshiftCommonUI")
isDialog: '@' isDialog: '@'
}, },
templateUrl: 'src/components/edit-project/editProject.html', templateUrl: 'src/components/edit-project/editProject.html',
controller: ["$scope", "$filter", "$location", "DataService", "annotationNameFilter", function($scope, $filter, $location, DataService, annotationNameFilter) { controller: ["$scope", "$filter", "$location", "DataService", "NotificationsService", "annotationNameFilter", "displayNameFilter", function($scope, $filter, $location, DataService, NotificationsService, annotationNameFilter, displayNameFilter) {
if(!($scope.submitButtonLabel)) { if(!($scope.submitButtonLabel)) {
$scope.submitButtonLabel = 'Save'; $scope.submitButtonLabel = 'Save';
} }
...@@ -654,6 +694,11 @@ angular.module("openshiftCommonUI") ...@@ -654,6 +694,11 @@ angular.module("openshiftCommonUI")
return resource; return resource;
}; };
var showAlert = function(alert) {
$scope.alerts["update"] = alert;
NotificationsService.addNotification(alert);
};
$scope.editableFields = editableFields($scope.project); $scope.editableFields = editableFields($scope.project);
$scope.update = function() { $scope.update = function() {
...@@ -666,20 +711,25 @@ angular.module("openshiftCommonUI") ...@@ -666,20 +711,25 @@ angular.module("openshiftCommonUI")
cleanEditableAnnotations(mergeEditable($scope.project, $scope.editableFields)), cleanEditableAnnotations(mergeEditable($scope.project, $scope.editableFields)),
{projectName: $scope.project.name}, {projectName: $scope.project.name},
{errorNotification: false}) {errorNotification: false})
.then(function() { .then(function(project) {
// angular is actually wrapping the redirect action :/ // angular is actually wrapping the redirect action :/
var cb = $scope.redirectAction(); var cb = $scope.redirectAction();
if (cb) { if (cb) {
cb(encodeURIComponent($scope.project.metadata.name)); cb(encodeURIComponent($scope.project.metadata.name));
} }
showAlert({
type: "success",
message: "Project \'" + displayNameFilter(project) + "\' was successfully updated."
});
}, function(result) { }, function(result) {
$scope.disableInputs = false; $scope.disableInputs = false;
$scope.editableFields = editableFields($scope.project); $scope.editableFields = editableFields($scope.project);
$scope.alerts["update"] = { showAlert({
type: "error", type: "error",
message: "An error occurred while updating the project", message: "An error occurred while updating the project",
details: $filter('getErrorDetails')(result) details: $filter('getErrorDetails')(result)
}; });
}); });
} }
}; };
...@@ -714,6 +764,55 @@ angular.module('openshiftCommonUI') ...@@ -714,6 +764,55 @@ angular.module('openshiftCommonUI')
;'use strict'; ;'use strict';
angular.module('openshiftCommonUI') angular.module('openshiftCommonUI')
.directive('toastNotifications', ["NotificationsService", "$timeout", function(NotificationsService, $timeout) {
return {
restrict: 'E',
scope: {},
templateUrl: 'src/components/toast-notifications/toast-notifications.html',
link: function($scope) {
$scope.notifications = NotificationsService.getNotifications();
$scope.close = function(notification) {
notification.hidden = true;
if (_.isFunction(notification.onClose)) {
notification.onClose();
}
};
$scope.onClick = function(notification, link) {
if (_.isFunction(link.onClick)) {
// If onClick() returns true, also hide the alert.
var close = link.onClick();
if (close) {
notification.hidden = true;
}
}
};
$scope.setHover = function(notification, isHover) {
notification.isHover = isHover;
};
$scope.$watch('notifications', function() {
_.each($scope.notifications, function(notification) {
if (NotificationsService.isAutoDismiss(notification) && !notification.hidden) {
if (!notification.timerId) {
notification.timerId = $timeout(function () {
notification.timerId = -1;
if (!notification.isHover) {
notification.hidden = true;
}
}, NotificationsService.dismissDelay);
} else if (notification.timerId === -1 && !notification.isHover) {
notification.hidden = true;
}
}
});
}, true);
}
};
}]);
;'use strict';
angular.module('openshiftCommonUI')
// Truncates text to a length, adding a tooltip and an ellipsis if truncated. // Truncates text to a length, adding a tooltip and an ellipsis if truncated.
// Different than `text-overflow: ellipsis` because it allows for multiline text. // Different than `text-overflow: ellipsis` because it allows for multiline text.
.directive('truncateLongText', ["truncateFilter", function(truncateFilter) { .directive('truncateLongText', ["truncateFilter", function(truncateFilter) {
...@@ -783,6 +882,51 @@ if (!window.OPENSHIFT_CONFIG) { ...@@ -783,6 +882,51 @@ if (!window.OPENSHIFT_CONFIG) {
}; };
} }
;'use strict'; ;'use strict';
angular.module('openshiftCommonUI')
.filter("alertStatus", function() {
return function (type) {
var status;
switch(type) {
case 'error':
status = 'alert-danger';
break;
case 'warning':
status = 'alert-warning';
break;
case 'success':
status = 'alert-success';
break;
default:
status = 'alert-info';
}
return status;
};
})
.filter('alertIcon', function() {
return function (type) {
var icon;
switch(type) {
case 'error':
icon = 'pficon pficon-error-circle-o';
break;
case 'warning':
icon = 'pficon pficon-warning-triangle-o';
break;
case 'success':
icon = 'pficon pficon-ok';
break;
default:
icon = 'pficon pficon-info';
}
return icon;
};
});
;'use strict';
/* jshint unused: false */ /* jshint unused: false */
angular.module('openshiftCommonUI') angular.module('openshiftCommonUI')
...@@ -1281,6 +1425,16 @@ angular.module('openshiftCommonUI') ...@@ -1281,6 +1425,16 @@ angular.module('openshiftCommonUI')
return Object.keys(hash).length; return Object.keys(hash).length;
}; };
}]) }])
// Wraps _.filter. Works with hashes, unlike ngFilter, which only works
// with arrays.
.filter('filterCollection', function() {
return function(collection, predicate) {
if (!collection || !predicate) {
return collection;
}
return _.filter(collection, predicate);
};
})
.filter('generateName', function() { .filter('generateName', function() {
return function(prefix, length) { return function(prefix, length) {
if (!prefix) { if (!prefix) {
...@@ -1292,7 +1446,22 @@ angular.module('openshiftCommonUI') ...@@ -1292,7 +1446,22 @@ angular.module('openshiftCommonUI')
var randomString = Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1); var randomString = Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1);
return prefix + randomString; return prefix + randomString;
}; };
}); })
.filter("getErrorDetails", ["upperFirstFilter", function(upperFirstFilter) {
return function(result, capitalize) {
var error = result.data || {};
if (error.message) {
return capitalize ? upperFirstFilter(error.message) : error.message;
}
var status = result.status || error.status;
if (status) {
return "Status: " + status;
}
return "";
};
}]);
;'use strict'; ;'use strict';
angular.module("openshiftCommonServices") angular.module("openshiftCommonServices")
...@@ -4343,3 +4512,75 @@ angular.module('openshiftCommonUI').factory('GuidedTourService', function() { ...@@ -4343,3 +4512,75 @@ angular.module('openshiftCommonUI').factory('GuidedTourService', function() {
cancelTour: cancelTour cancelTour: cancelTour
}; };
}); });
;'use strict';
angular.module('openshiftCommonServices').provider('NotificationsService', function() {
this.dismissDelay = 8000;
this.autoDismissTypes = ['info', 'success'];
this.$get = function() {
var notifications = [];
var dismissDelay = this.dismissDelay;
var autoDismissTypes = this.autoDismissTypes;
var notificationHiddenKey = function(notificationID, namespace) {
if (!namespace) {
return 'hide/notification/' + notificationID;
}
return 'hide/notification/' + namespace + '/' + notificationID;
};
var addNotification = function (notification, notificationID, namespace) {
if (notificationID && isNotificationPermanentlyHidden(notificationID, namespace)) {
notification.hidden = true;
}
notifications.push(notification);
};
var getNotifications = function () {
return notifications;
};
var clearNotifications = function () {
_.take(notifications, 0);
};
var isNotificationPermanentlyHidden = function (notificationID, namespace) {
var key = notificationHiddenKey(notificationID, namespace);
return localStorage.getItem(key) === 'true';
};
var permanentlyHideNotification = function (notificationID, namespace) {
var key = notificationHiddenKey(notificationID, namespace);
localStorage.setItem(key, 'true');
};
var isAutoDismiss = function(notification) {
return _.find(autoDismissTypes, function(type) {
return type === notification.type;
});
};
return {
addNotification: addNotification,
getNotifications: getNotifications,
clearNotifications: clearNotifications,
isNotificationPermanentlyHidden: isNotificationPermanentlyHidden,
permanentlyHideNotification: permanentlyHideNotification,
isAutoDismiss: isAutoDismiss,
dismissDelay: dismissDelay,
autoDismissTypes: autoDismissTypes
};
};
this.setDismissDelay = function(delayInMs) {
this.dismissDelay = delayInMs;
};
this.setAutoDismissTypes = function(arrayOfTypes) {
this.autoDismissTypes = arrayOfTypes;
};
});
...@@ -78,6 +78,7 @@ allDeferreds = allDeferreds.concat(additionalDeferreds), $.when.apply(this, allD ...@@ -78,6 +78,7 @@ allDeferreds = allDeferreds.concat(additionalDeferreds), $.when.apply(this, allD
$templateCache.put("src/components/create-project/createProject.html", '<form name="createProjectForm" novalidate>\n <fieldset ng-disabled="disableInputs">\n <div class="form-group">\n <label for="name" class="required">Name</label>\n <span ng-class="{\'has-error\': (createProjectForm.name.$error.pattern && createProjectForm.name.$touched) || nameTaken}">\n <input class="form-control input-lg"\n name="name"\n id="name"\n placeholder="my-project"\n type="text"\n required\n take-focus\n minlength="2"\n maxlength="63"\n pattern="[a-z0-9]([-a-z0-9]*[a-z0-9])?"\n aria-describedby="nameHelp"\n ng-model="name"\n ng-model-options="{ updateOn: \'default blur\' }"\n ng-change="nameTaken = false"\n autocorrect="off"\n autocapitalize="off"\n spellcheck="false">\n </span>\n <div>\n <span class="help-block">A unique name for the project.</span>\n </div>\n <div class="has-error">\n <span id="nameHelp" class="help-block" ng-if="createProjectForm.name.$error.required && createProjectForm.name.$dirty">\n Name is required.\n </span>\n </div>\n <div class="has-error">\n <span id="nameHelp" class="help-block" ng-if="createProjectForm.name.$error.minlength && createProjectForm.name.$touched">\n Name must have at least two characters.\n </span>\n </div>\n <div class="has-error">\n <span id="nameHelp" class="help-block" ng-if="createProjectForm.name.$error.pattern && createProjectForm.name.$touched">\n Project names may only contain lower-case letters, numbers, and dashes.\n They may not start or end with a dash.\n </span>\n </div>\n <div class="has-error">\n <span class="help-block" ng-if="nameTaken">\n This name is already in use. Please choose a different name.\n </span>\n </div>\n </div>\n\n <div class="form-group">\n <label for="displayName">Display Name</label>\n <input class="form-control input-lg"\n name="displayName"\n id="displayName"\n placeholder="My Project"\n type="text"\n ng-model="displayName">\n </div>\n\n <div class="form-group">\n <label for="description">Description</label>\n <textarea class="form-control input-lg"\n name="description"\n id="description"\n placeholder="A short description."\n ng-model="description"></textarea>\n </div>\n\n <div class="button-group">\n <button type="submit"\n class="btn btn-primary btn-lg"\n ng-class="{\'dialog-btn\': isDialog}"\n ng-click="createProject()"\n ng-disabled="createProjectForm.$invalid || nameTaken || disableInputs"\n value="">\n Create\n </button>\n <button\n class="btn btn-default btn-lg"\n ng-class="{\'dialog-btn\': isDialog}"\n ng-click="cancelCreateProject()">\n Cancel\n </button>\n </div>\n </fieldset>\n</form>\n'), $templateCache.put("src/components/create-project/createProject.html", '<form name="createProjectForm" novalidate>\n <fieldset ng-disabled="disableInputs">\n <div class="form-group">\n <label for="name" class="required">Name</label>\n <span ng-class="{\'has-error\': (createProjectForm.name.$error.pattern && createProjectForm.name.$touched) || nameTaken}">\n <input class="form-control input-lg"\n name="name"\n id="name"\n placeholder="my-project"\n type="text"\n required\n take-focus\n minlength="2"\n maxlength="63"\n pattern="[a-z0-9]([-a-z0-9]*[a-z0-9])?"\n aria-describedby="nameHelp"\n ng-model="name"\n ng-model-options="{ updateOn: \'default blur\' }"\n ng-change="nameTaken = false"\n autocorrect="off"\n autocapitalize="off"\n spellcheck="false">\n </span>\n <div>\n <span class="help-block">A unique name for the project.</span>\n </div>\n <div class="has-error">\n <span id="nameHelp" class="help-block" ng-if="createProjectForm.name.$error.required && createProjectForm.name.$dirty">\n Name is required.\n </span>\n </div>\n <div class="has-error">\n <span id="nameHelp" class="help-block" ng-if="createProjectForm.name.$error.minlength && createProjectForm.name.$touched">\n Name must have at least two characters.\n </span>\n </div>\n <div class="has-error">\n <span id="nameHelp" class="help-block" ng-if="createProjectForm.name.$error.pattern && createProjectForm.name.$touched">\n Project names may only contain lower-case letters, numbers, and dashes.\n They may not start or end with a dash.\n </span>\n </div>\n <div class="has-error">\n <span class="help-block" ng-if="nameTaken">\n This name is already in use. Please choose a different name.\n </span>\n </div>\n </div>\n\n <div class="form-group">\n <label for="displayName">Display Name</label>\n <input class="form-control input-lg"\n name="displayName"\n id="displayName"\n placeholder="My Project"\n type="text"\n ng-model="displayName">\n </div>\n\n <div class="form-group">\n <label for="description">Description</label>\n <textarea class="form-control input-lg"\n name="description"\n id="description"\n placeholder="A short description."\n ng-model="description"></textarea>\n </div>\n\n <div class="button-group">\n <button type="submit"\n class="btn btn-primary btn-lg"\n ng-class="{\'dialog-btn\': isDialog}"\n ng-click="createProject()"\n ng-disabled="createProjectForm.$invalid || nameTaken || disableInputs"\n value="">\n Create\n </button>\n <button\n class="btn btn-default btn-lg"\n ng-class="{\'dialog-btn\': isDialog}"\n ng-click="cancelCreateProject()">\n Cancel\n </button>\n </div>\n </fieldset>\n</form>\n'),
$templateCache.put("src/components/delete-project/delete-project-button.html", '<div class="actions">\n <!-- Avoid whitespace inside the link -->\n <a href=""\n ng-click="$event.stopPropagation(); openDeleteModal()"\n role="button"\n class="action-button"\n ng-attr-aria-disabled="{{disableDelete ? \'true\' : undefined}}"\n ng-class="{ \'disabled-link\': disableDelete }"\n ><i class="fa fa-trash-o" aria-hidden="true"\n ></i><span class="sr-only">Delete Project {{projectName}}</span></a>\n</div>\n'), $templateCache.put("src/components/delete-project/delete-project-modal.html", '<div class="delete-resource-modal">\n <!-- Use a form so that the enter key submits when typing a project name to confirm. -->\n <form>\n <div class="modal-body">\n <h1>Are you sure you want to delete the project\n \'<strong>{{displayName ? displayName : projectName}}</strong>\'?</h1>\n <p>\n This will <strong>delete all resources</strong> associated with\n the project {{displayName ? displayName : projectName}} and <strong>cannot be\n undone</strong>. Make sure this is something you really want to do!\n </p>\n <div ng-show="typeNameToConfirm">\n <p>Type the name of the project to confirm.</p>\n <p>\n <label class="sr-only" for="resource-to-delete">project to delete</label>\n <input\n ng-model="confirmName"\n id="resource-to-delete"\n type="text"\n class="form-control input-lg"\n autocorrect="off"\n autocapitalize="off"\n spellcheck="false"\n autofocus>\n </p>\n </div>\n </div>\n <div class="modal-footer">\n <button ng-disabled="typeNameToConfirm && confirmName !== projectName && confirmName !== displayName" class="btn btn-lg btn-danger" type="submit" ng-click="delete();">Delete</button>\n <button class="btn btn-lg btn-default" type="button" ng-click="cancel();">Cancel</button>\n </div>\n </form>\n</div>\n'), $templateCache.put("src/components/delete-project/delete-project-button.html", '<div class="actions">\n <!-- Avoid whitespace inside the link -->\n <a href=""\n ng-click="$event.stopPropagation(); openDeleteModal()"\n role="button"\n class="action-button"\n ng-attr-aria-disabled="{{disableDelete ? \'true\' : undefined}}"\n ng-class="{ \'disabled-link\': disableDelete }"\n ><i class="fa fa-trash-o" aria-hidden="true"\n ></i><span class="sr-only">Delete Project {{projectName}}</span></a>\n</div>\n'), $templateCache.put("src/components/delete-project/delete-project-modal.html", '<div class="delete-resource-modal">\n <!-- Use a form so that the enter key submits when typing a project name to confirm. -->\n <form>\n <div class="modal-body">\n <h1>Are you sure you want to delete the project\n \'<strong>{{displayName ? displayName : projectName}}</strong>\'?</h1>\n <p>\n This will <strong>delete all resources</strong> associated with\n the project {{displayName ? displayName : projectName}} and <strong>cannot be\n undone</strong>. Make sure this is something you really want to do!\n </p>\n <div ng-show="typeNameToConfirm">\n <p>Type the name of the project to confirm.</p>\n <p>\n <label class="sr-only" for="resource-to-delete">project to delete</label>\n <input\n ng-model="confirmName"\n id="resource-to-delete"\n type="text"\n class="form-control input-lg"\n autocorrect="off"\n autocapitalize="off"\n spellcheck="false"\n autofocus>\n </p>\n </div>\n </div>\n <div class="modal-footer">\n <button ng-disabled="typeNameToConfirm && confirmName !== projectName && confirmName !== displayName" class="btn btn-lg btn-danger" type="submit" ng-click="delete();">Delete</button>\n <button class="btn btn-lg btn-default" type="button" ng-click="cancel();">Cancel</button>\n </div>\n </form>\n</div>\n'),
$templateCache.put("src/components/delete-project/delete-project.html", '<a href="javascript:void(0)"\n ng-click="openDeleteModal()"\n role="button"\n ng-attr-aria-disabled="{{disableDelete ? \'true\' : undefined}}"\n ng-class="{ \'disabled-link\': disableDelete }"\n>{{label || \'Delete\'}}</a>\n'), $templateCache.put("src/components/edit-project/editProject.html", '<form name="editProjectForm">\n <fieldset ng-disabled="disableInputs">\n <div class="form-group">\n <label for="displayName">Display Name</label>\n <input class="form-control input-lg"\n name="displayName"\n id="displayName"\n placeholder="My Project"\n type="text"\n ng-model="editableFields.displayName">\n </div>\n\n <div class="form-group">\n <label for="description">Description</label>\n <textarea class="form-control input-lg"\n name="description"\n id="description"\n placeholder="A short description."\n ng-model="editableFields.description"></textarea>\n </div>\n\n <div class="button-group">\n <button type="submit"\n class="btn btn-primary btn-lg"\n ng-class="{\'dialog-btn\': isDialog}"\n ng-click="update()"\n ng-disabled="editProjectForm.$invalid || disableInputs"\n value="">{{submitButtonLabel}}</button>\n <button\n class="btn btn-default btn-lg"\n ng-class="{\'dialog-btn\': isDialog}"\n ng-click="cancelEditProject()">\n Cancel\n </button>\n </div>\n </fieldset>\n</form>\n'), $templateCache.put("src/components/delete-project/delete-project.html", '<a href="javascript:void(0)"\n ng-click="openDeleteModal()"\n role="button"\n ng-attr-aria-disabled="{{disableDelete ? \'true\' : undefined}}"\n ng-class="{ \'disabled-link\': disableDelete }"\n>{{label || \'Delete\'}}</a>\n'), $templateCache.put("src/components/edit-project/editProject.html", '<form name="editProjectForm">\n <fieldset ng-disabled="disableInputs">\n <div class="form-group">\n <label for="displayName">Display Name</label>\n <input class="form-control input-lg"\n name="displayName"\n id="displayName"\n placeholder="My Project"\n type="text"\n ng-model="editableFields.displayName">\n </div>\n\n <div class="form-group">\n <label for="description">Description</label>\n <textarea class="form-control input-lg"\n name="description"\n id="description"\n placeholder="A short description."\n ng-model="editableFields.description"></textarea>\n </div>\n\n <div class="button-group">\n <button type="submit"\n class="btn btn-primary btn-lg"\n ng-class="{\'dialog-btn\': isDialog}"\n ng-click="update()"\n ng-disabled="editProjectForm.$invalid || disableInputs"\n value="">{{submitButtonLabel}}</button>\n <button\n class="btn btn-default btn-lg"\n ng-class="{\'dialog-btn\': isDialog}"\n ng-click="cancelEditProject()">\n Cancel\n </button>\n </div>\n </fieldset>\n</form>\n'),
$templateCache.put("src/components/toast-notifications/toast-notifications.html", '<div class="toast-notifications-list-pf">\n <div ng-repeat="(notificationID, notification) in notifications track by (notificationID + (notification.message || notification.details))" ng-if="!notification.hidden"\n ng-mouseenter="setHover(notification, true)" ng-mouseleave="setHover(notification, false)">\n <div class="toast-pf alert {{notification.type | alertStatus}}" ng-class="{\'alert-dismissable\': !hideCloseButton}">\n <button ng-if="!hideCloseButton" type="button" class="close" ng-click="close(notification)">\n <span class="pficon pficon-close" aria-hidden="true"></span>\n <span class="sr-only">Close</span>\n </button>\n <span class="{{notification.type | alertIcon}}" aria-hidden="true"></span>\n <span class="sr-only">{{notification.type}}</span>\n <span class="toast-notification-message" ng-if="notification.message">{{notification.message}}</span>\n <span ng-if="notification.details">{{notification.details}}</span>\n <span ng-repeat="link in notification.links">\n <a ng-if="!link.href" href="" ng-click="onClick(notification, link)" role="button">{{link.label}}</a>\n <a ng-if="link.href" ng-href="{{link.href}}" ng-attr-target="{{link.target}}">{{link.label}}</a>\n <span ng-if="!$last" class="toast-action-divider">|</span>\n </span>\n </div>\n </div>\n</div>\n'),
$templateCache.put("src/components/truncate-long-text/truncateLongText.html", '<!--\n Do not remove class `truncated-content` (here or below) even though it\'s not\n styled directly in origin-web-common. `truncated-content` is used by\n origin-web-console in certain contexts.\n-->\n<span ng-if="!truncated" ng-bind-html="content | highlightKeywords : keywords" class="truncated-content"></span>\n<span ng-if="truncated">\n <span ng-if="!toggles.expanded">\n <span ng-attr-title="{{content}}" class="truncation-block">\n <span ng-bind-html="truncatedContent | highlightKeywords : keywords" class="truncated-content"></span>&hellip;\n </span>\n <a ng-if="expandable" href="" ng-click="toggles.expanded = true" class="nowrap">See All</a>\n </span>\n <span ng-if="toggles.expanded">\n <div ng-if="prettifyJson" class="well">\n <span class="pull-right" style="margin-top: -10px;"><a href="" ng-click="toggles.expanded = false" class="truncation-collapse-link">Collapse</a></span>\n <span ng-bind-html="content | prettifyJSON | highlightKeywords : keywords" class="pretty-json truncated-content"></span>\n </div>\n <span ng-if="!prettifyJson">\n <span class="pull-right"><a href="" ng-click="toggles.expanded = false" class="truncation-collapse-link">Collapse</a></span>\n <span ng-bind-html="content | highlightKeywords : keywords" class="truncated-content"></span>\n </span>\n </span>\n</span>\n'); $templateCache.put("src/components/truncate-long-text/truncateLongText.html", '<!--\n Do not remove class `truncated-content` (here or below) even though it\'s not\n styled directly in origin-web-common. `truncated-content` is used by\n origin-web-console in certain contexts.\n-->\n<span ng-if="!truncated" ng-bind-html="content | highlightKeywords : keywords" class="truncated-content"></span>\n<span ng-if="truncated">\n <span ng-if="!toggles.expanded">\n <span ng-attr-title="{{content}}" class="truncation-block">\n <span ng-bind-html="truncatedContent | highlightKeywords : keywords" class="truncated-content"></span>&hellip;\n </span>\n <a ng-if="expandable" href="" ng-click="toggles.expanded = true" class="nowrap">See All</a>\n </span>\n <span ng-if="toggles.expanded">\n <div ng-if="prettifyJson" class="well">\n <span class="pull-right" style="margin-top: -10px;"><a href="" ng-click="toggles.expanded = false" class="truncation-collapse-link">Collapse</a></span>\n <span ng-bind-html="content | prettifyJSON | highlightKeywords : keywords" class="pretty-json truncated-content"></span>\n </div>\n <span ng-if="!prettifyJson">\n <span class="pull-right"><a href="" ng-click="toggles.expanded = false" class="truncation-collapse-link">Collapse</a></span>\n <span ng-bind-html="content | highlightKeywords : keywords" class="truncated-content"></span>\n </span>\n </span>\n</span>\n');
} ]), angular.module("openshiftCommonUI").directive("createProject", [ "$window", function($window) { } ]), angular.module("openshiftCommonUI").directive("createProject", [ "$window", function($window) {
return { return {
...@@ -89,8 +90,12 @@ onCancel:"&?", ...@@ -89,8 +90,12 @@ onCancel:"&?",
isDialog:"@" isDialog:"@"
}, },
templateUrl:"src/components/create-project/createProject.html", templateUrl:"src/components/create-project/createProject.html",
controller:[ "$scope", "$filter", "$location", "DataService", function($scope, $filter, $location, DataService) { controller:[ "$scope", "$filter", "$location", "DataService", "NotificationsService", "displayNameFilter", function($scope, $filter, $location, DataService, NotificationsService, displayNameFilter) {
$scope.isDialog = "true" === $scope.isDialog, $scope.createProject = function() { $scope.submitButtonLabel || ($scope.submitButtonLabel = "Create"), $scope.isDialog = "true" === $scope.isDialog;
var showAlert = function(name, alert) {
$scope.alerts[name] = alert, NotificationsService.addNotification(alert);
};
$scope.createProject = function() {
$scope.disableInputs = !0, $scope.createProjectForm.$valid && DataService.create("projectrequests", null, { $scope.disableInputs = !0, $scope.createProjectForm.$valid && DataService.create("projectrequests", null, {
apiVersion:"v1", apiVersion:"v1",
kind:"ProjectRequest", kind:"ProjectRequest",
...@@ -101,16 +106,19 @@ displayName:$scope.displayName, ...@@ -101,16 +106,19 @@ displayName:$scope.displayName,
description:$scope.description description:$scope.description
}, $scope).then(function(data) { }, $scope).then(function(data) {
var cb = $scope.redirectAction(); var cb = $scope.redirectAction();
cb ? cb(encodeURIComponent(data.metadata.name)) :$location.path("project/" + encodeURIComponent(data.metadata.name) + "/create"); cb ? cb(encodeURIComponent(data.metadata.name)) :$location.path("project/" + encodeURIComponent(data.metadata.name) + "/create"), showAlert("created-project", {
type:"success",
message:"Project '" + displayNameFilter(data) + "' was successfully created."
});
}, function(result) { }, function(result) {
$scope.disableInputs = !1; $scope.disableInputs = !1;
var data = result.data || {}; var data = result.data || {};
if ("AlreadyExists" === data.reason) $scope.nameTaken = !0; else { if ("AlreadyExists" === data.reason) $scope.nameTaken = !0; else {
var msg = data.message || "An error occurred creating the project."; var msg = data.message || "An error occurred creating the project.";
$scope.alerts["error-creating-project"] = { showAlert("error-creating-project", {
type:"error", type:"error",
message:msg message:msg
}; });
} }
}); });
}, $scope.cancelCreateProject = function() { }, $scope.cancelCreateProject = function() {
...@@ -121,7 +129,7 @@ cb && cb(); ...@@ -121,7 +129,7 @@ cb && cb();
}; };
} ] } ]
}; };
} ]), angular.module("openshiftCommonUI").directive("deleteProject", [ "$uibModal", "$location", "$filter", "$q", "hashSizeFilter", "APIService", "DataService", "AlertMessageService", "Logger", function($uibModal, $location, $filter, $q, hashSizeFilter, APIService, DataService, AlertMessageService, Logger) { } ]), angular.module("openshiftCommonUI").directive("deleteProject", [ "$uibModal", "$location", "$filter", "$q", "hashSizeFilter", "APIService", "DataService", "AlertMessageService", "NotificationsService", "Logger", function($uibModal, $location, $filter, $q, hashSizeFilter, APIService, DataService, AlertMessageService, NotificationsService, Logger) {
return { return {
restrict:"E", restrict:"E",
scope:{ scope:{
...@@ -142,7 +150,7 @@ return angular.isDefined(attr.buttonOnly) ? "src/components/delete-project/delet ...@@ -142,7 +150,7 @@ return angular.isDefined(attr.buttonOnly) ? "src/components/delete-project/delet
replace:!0, replace:!0,
link:function(scope, element, attrs) { link:function(scope, element, attrs) {
var showAlert = function(alert) { var showAlert = function(alert) {
scope.stayOnCurrentPage ? scope.alerts[alert.name] = alert.data :AlertMessageService.addAlert(alert); scope.stayOnCurrentPage ? scope.alerts[alert.name] = alert.data :AlertMessageService.addAlert(alert), NotificationsService.addNotification(alert.data);
}, navigateToList = function() { }, navigateToList = function() {
if (!scope.stayOnCurrentPage) { if (!scope.stayOnCurrentPage) {
if (scope.redirectUrl) return void $location.url(scope.redirectUrl); if (scope.redirectUrl) return void $location.url(scope.redirectUrl);
...@@ -172,11 +180,12 @@ message:_.capitalize(formattedResource) + " was marked for deletion." ...@@ -172,11 +180,12 @@ message:_.capitalize(formattedResource) + " was marked for deletion."
} }
}), scope.success && scope.success(), navigateToList(); }), scope.success && scope.success(), navigateToList();
})["catch"](function(err) { })["catch"](function(err) {
scope.alerts[projectName] = { var alert = {
type:"error", type:"error",
message:_.capitalize(formattedResource) + "' could not be deleted.", message:_.capitalize(formattedResource) + "' could not be deleted.",
details:$filter("getErrorDetails")(err) details:$filter("getErrorDetails")(err)
}, Logger.error(formattedResource + " could not be deleted.", err); };
scope.alerts[projectName] = alert, NotificationsService.addNotification(alert), Logger.error(formattedResource + " could not be deleted.", err);
}); });
}); });
} }
...@@ -201,7 +210,7 @@ onCancel:"&", ...@@ -201,7 +210,7 @@ onCancel:"&",
isDialog:"@" isDialog:"@"
}, },
templateUrl:"src/components/edit-project/editProject.html", templateUrl:"src/components/edit-project/editProject.html",
controller:[ "$scope", "$filter", "$location", "DataService", "annotationNameFilter", function($scope, $filter, $location, DataService, annotationNameFilter) { controller:[ "$scope", "$filter", "$location", "DataService", "NotificationsService", "annotationNameFilter", "displayNameFilter", function($scope, $filter, $location, DataService, NotificationsService, annotationNameFilter, displayNameFilter) {
$scope.submitButtonLabel || ($scope.submitButtonLabel = "Save"), $scope.isDialog = "true" === $scope.isDialog; $scope.submitButtonLabel || ($scope.submitButtonLabel = "Save"), $scope.isDialog = "true" === $scope.isDialog;
var annotation = $filter("annotation"), annotationName = $filter("annotationName"), editableFields = function(resource) { var annotation = $filter("annotation"), annotationName = $filter("annotationName"), editableFields = function(resource) {
return { return {
...@@ -216,21 +225,26 @@ var paths = [ annotationNameFilter("description"), annotationNameFilter("display ...@@ -216,21 +225,26 @@ var paths = [ annotationNameFilter("description"), annotationNameFilter("display
return _.each(paths, function(path) { return _.each(paths, function(path) {
resource.metadata.annotations[path] || delete resource.metadata.annotations[path]; resource.metadata.annotations[path] || delete resource.metadata.annotations[path];
}), resource; }), resource;
}, showAlert = function(alert) {
$scope.alerts.update = alert, NotificationsService.addNotification(alert);
}; };
$scope.editableFields = editableFields($scope.project), $scope.update = function() { $scope.editableFields = editableFields($scope.project), $scope.update = function() {
$scope.disableInputs = !0, $scope.editProjectForm.$valid && DataService.update("projects", $scope.project.metadata.name, cleanEditableAnnotations(mergeEditable($scope.project, $scope.editableFields)), { $scope.disableInputs = !0, $scope.editProjectForm.$valid && DataService.update("projects", $scope.project.metadata.name, cleanEditableAnnotations(mergeEditable($scope.project, $scope.editableFields)), {
projectName:$scope.project.name projectName:$scope.project.name
}, { }, {
errorNotification:!1 errorNotification:!1
}).then(function() { }).then(function(project) {
var cb = $scope.redirectAction(); var cb = $scope.redirectAction();
cb && cb(encodeURIComponent($scope.project.metadata.name)); cb && cb(encodeURIComponent($scope.project.metadata.name)), showAlert({
type:"success",
message:"Project '" + displayNameFilter(project) + "' was successfully updated."
});
}, function(result) { }, function(result) {
$scope.disableInputs = !1, $scope.editableFields = editableFields($scope.project), $scope.alerts.update = { $scope.disableInputs = !1, $scope.editableFields = editableFields($scope.project), showAlert({
type:"error", type:"error",
message:"An error occurred while updating the project", message:"An error occurred while updating the project",
details:$filter("getErrorDetails")(result) details:$filter("getErrorDetails")(result)
}; });
}); });
}, $scope.cancelEditProject = function() { }, $scope.cancelEditProject = function() {
var cb = $scope.onCancel(); var cb = $scope.onCancel();
...@@ -247,6 +261,30 @@ $(element).focus(); ...@@ -247,6 +261,30 @@ $(element).focus();
}, 300); }, 300);
} }
}; };
} ]), angular.module("openshiftCommonUI").directive("toastNotifications", [ "NotificationsService", "$timeout", function(NotificationsService, $timeout) {
return {
restrict:"E",
scope:{},
templateUrl:"src/components/toast-notifications/toast-notifications.html",
link:function($scope) {
$scope.notifications = NotificationsService.getNotifications(), $scope.close = function(notification) {
notification.hidden = !0, _.isFunction(notification.onClose) && notification.onClose();
}, $scope.onClick = function(notification, link) {
if (_.isFunction(link.onClick)) {
var close = link.onClick();
close && (notification.hidden = !0);
}
}, $scope.setHover = function(notification, isHover) {
notification.isHover = isHover;
}, $scope.$watch("notifications", function() {
_.each($scope.notifications, function(notification) {
NotificationsService.isAutoDismiss(notification) && !notification.hidden && (notification.timerId ? -1 !== notification.timerId || notification.isHover || (notification.hidden = !0) :notification.timerId = $timeout(function() {
notification.timerId = -1, notification.isHover || (notification.hidden = !0);
}, NotificationsService.dismissDelay));
});
}, !0);
}
};
} ]), angular.module("openshiftCommonUI").directive("truncateLongText", [ "truncateFilter", function(truncateFilter) { } ]), angular.module("openshiftCommonUI").directive("truncateLongText", [ "truncateFilter", function(truncateFilter) {
return { return {
restrict:"E", restrict:"E",
...@@ -295,6 +333,48 @@ metricsURL:"" ...@@ -295,6 +333,48 @@ metricsURL:""
}, window.OPENSHIFT_VERSION = { }, window.OPENSHIFT_VERSION = {
openshift:"dev-mode", openshift:"dev-mode",
kubernetes:"dev-mode" kubernetes:"dev-mode"
}), angular.module("openshiftCommonUI").filter("alertStatus", function() {
return function(type) {
var status;
switch (type) {
case "error":
status = "alert-danger";
break;
case "warning":
status = "alert-warning";
break;
case "success":
status = "alert-success";
break;
default:
status = "alert-info";
}
return status;
};
}).filter("alertIcon", function() {
return function(type) {
var icon;
switch (type) {
case "error":
icon = "pficon pficon-error-circle-o";
break;
case "warning":
icon = "pficon pficon-warning-triangle-o";
break;
case "success":
icon = "pficon pficon-ok";
break;
default:
icon = "pficon pficon-info";
}
return icon;
};
}), angular.module("openshiftCommonUI").filter("annotationName", function() { }), angular.module("openshiftCommonUI").filter("annotationName", function() {
var annotationMap = { var annotationMap = {
buildConfig:[ "openshift.io/build-config.name" ], buildConfig:[ "openshift.io/build-config.name" ],
...@@ -508,13 +588,24 @@ return _.size; ...@@ -508,13 +588,24 @@ return _.size;
return function(hash) { return function(hash) {
return hash ? Object.keys(hash).length :0; return hash ? Object.keys(hash).length :0;
}; };
} ]).filter("generateName", function() { } ]).filter("filterCollection", function() {
return function(collection, predicate) {
return collection && predicate ? _.filter(collection, predicate) :collection;
};
}).filter("generateName", function() {
return function(prefix, length) { return function(prefix, length) {
prefix || (prefix = ""), length || (length = 5); prefix || (prefix = ""), length || (length = 5);
var randomString = Math.round(Math.pow(36, length + 1) - Math.random() * Math.pow(36, length)).toString(36).slice(1); var randomString = Math.round(Math.pow(36, length + 1) - Math.random() * Math.pow(36, length)).toString(36).slice(1);
return prefix + randomString; return prefix + randomString;
}; };
}), angular.module("openshiftCommonServices").service("AlertMessageService", function() { }).filter("getErrorDetails", [ "upperFirstFilter", function(upperFirstFilter) {
return function(result, capitalize) {
var error = result.data || {};
if (error.message) return capitalize ? upperFirstFilter(error.message) :error.message;
var status = result.status || error.status;
return status ? "Status: " + status :"";
};
} ]), angular.module("openshiftCommonServices").service("AlertMessageService", function() {
var alerts = [], alertHiddenKey = function(alertID, namespace) { var alerts = [], alertHiddenKey = function(alertID, namespace) {
return namespace ? "hide/alert/" + namespace + "/" + alertID :"hide/alert/" + alertID; return namespace ? "hide/alert/" + namespace + "/" + alertID :"hide/alert/" + alertID;
}; };
...@@ -1883,4 +1974,40 @@ return { ...@@ -1883,4 +1974,40 @@ return {
startTour:startTour, startTour:startTour,
cancelTour:cancelTour cancelTour:cancelTour
}; };
}), angular.module("openshiftCommonServices").provider("NotificationsService", function() {
this.dismissDelay = 8e3, this.autoDismissTypes = [ "info", "success" ], this.$get = function() {
var notifications = [], dismissDelay = this.dismissDelay, autoDismissTypes = this.autoDismissTypes, notificationHiddenKey = function(notificationID, namespace) {
return namespace ? "hide/notification/" + namespace + "/" + notificationID :"hide/notification/" + notificationID;
}, addNotification = function(notification, notificationID, namespace) {
notificationID && isNotificationPermanentlyHidden(notificationID, namespace) && (notification.hidden = !0), notifications.push(notification);
}, getNotifications = function() {
return notifications;
}, clearNotifications = function() {
_.take(notifications, 0);
}, isNotificationPermanentlyHidden = function(notificationID, namespace) {
var key = notificationHiddenKey(notificationID, namespace);
return "true" === localStorage.getItem(key);
}, permanentlyHideNotification = function(notificationID, namespace) {
var key = notificationHiddenKey(notificationID, namespace);
localStorage.setItem(key, "true");
}, isAutoDismiss = function(notification) {
return _.find(autoDismissTypes, function(type) {
return type === notification.type;
});
};
return {
addNotification:addNotification,
getNotifications:getNotifications,
clearNotifications:clearNotifications,
isNotificationPermanentlyHidden:isNotificationPermanentlyHidden,
permanentlyHideNotification:permanentlyHideNotification,
isAutoDismiss:isAutoDismiss,
dismissDelay:dismissDelay,
autoDismissTypes:autoDismissTypes
};
}, this.setDismissDelay = function(delayInMs) {
this.dismissDelay = delayInMs;
}, this.setAutoDismissTypes = function(arrayOfTypes) {
this.autoDismissTypes = arrayOfTypes;
};
}); });
\ No newline at end of file
...@@ -194,6 +194,30 @@ angular.module('openshiftCommonUI').run(['$templateCache', function($templateCac ...@@ -194,6 +194,30 @@ angular.module('openshiftCommonUI').run(['$templateCache', function($templateCac
); );
$templateCache.put('src/components/toast-notifications/toast-notifications.html',
"<div class=\"toast-notifications-list-pf\">\n" +
" <div ng-repeat=\"(notificationID, notification) in notifications track by (notificationID + (notification.message || notification.details))\" ng-if=\"!notification.hidden\"\n" +
" ng-mouseenter=\"setHover(notification, true)\" ng-mouseleave=\"setHover(notification, false)\">\n" +
" <div class=\"toast-pf alert {{notification.type | alertStatus}}\" ng-class=\"{'alert-dismissable': !hideCloseButton}\">\n" +
" <button ng-if=\"!hideCloseButton\" type=\"button\" class=\"close\" ng-click=\"close(notification)\">\n" +
" <span class=\"pficon pficon-close\" aria-hidden=\"true\"></span>\n" +
" <span class=\"sr-only\">Close</span>\n" +
" </button>\n" +
" <span class=\"{{notification.type | alertIcon}}\" aria-hidden=\"true\"></span>\n" +
" <span class=\"sr-only\">{{notification.type}}</span>\n" +
" <span class=\"toast-notification-message\" ng-if=\"notification.message\">{{notification.message}}</span>\n" +
" <span ng-if=\"notification.details\">{{notification.details}}</span>\n" +
" <span ng-repeat=\"link in notification.links\">\n" +
" <a ng-if=\"!link.href\" href=\"\" ng-click=\"onClick(notification, link)\" role=\"button\">{{link.label}}</a>\n" +
" <a ng-if=\"link.href\" ng-href=\"{{link.href}}\" ng-attr-target=\"{{link.target}}\">{{link.label}}</a>\n" +
" <span ng-if=\"!$last\" class=\"toast-action-divider\">|</span>\n" +
" </span>\n" +
" </div>\n" +
" </div>\n" +
"</div>\n"
);
$templateCache.put('src/components/truncate-long-text/truncateLongText.html', $templateCache.put('src/components/truncate-long-text/truncateLongText.html',
"<!--\n" + "<!--\n" +
" Do not remove class `truncated-content` (here or below) even though it's not\n" + " Do not remove class `truncated-content` (here or below) even though it's not\n" +
......
...@@ -12,9 +12,18 @@ angular.module("openshiftCommonUI") ...@@ -12,9 +12,18 @@ angular.module("openshiftCommonUI")
isDialog: '@' isDialog: '@'
}, },
templateUrl: 'src/components/create-project/createProject.html', templateUrl: 'src/components/create-project/createProject.html',
controller: function($scope, $filter, $location, DataService) { controller: function($scope, $filter, $location, DataService, NotificationsService, displayNameFilter) {
if(!($scope.submitButtonLabel)) {
$scope.submitButtonLabel = 'Create';
}
$scope.isDialog = $scope.isDialog === 'true'; $scope.isDialog = $scope.isDialog === 'true';
var showAlert = function(name, alert) {
$scope.alerts[name] = alert;
NotificationsService.addNotification(alert);
};
$scope.createProject = function() { $scope.createProject = function() {
$scope.disableInputs = true; $scope.disableInputs = true;
if ($scope.createProjectForm.$valid) { if ($scope.createProjectForm.$valid) {
...@@ -36,6 +45,10 @@ angular.module("openshiftCommonUI") ...@@ -36,6 +45,10 @@ angular.module("openshiftCommonUI")
} else { } else {
$location.path("project/" + encodeURIComponent(data.metadata.name) + "/create"); $location.path("project/" + encodeURIComponent(data.metadata.name) + "/create");
} }
showAlert('created-project', {
type: "success",
message: "Project \'" + displayNameFilter(data) + "\' was successfully created."
});
}, function(result) { }, function(result) {
$scope.disableInputs = false; $scope.disableInputs = false;
var data = result.data || {}; var data = result.data || {};
...@@ -43,7 +56,7 @@ angular.module("openshiftCommonUI") ...@@ -43,7 +56,7 @@ angular.module("openshiftCommonUI")
$scope.nameTaken = true; $scope.nameTaken = true;
} else { } else {
var msg = data.message || 'An error occurred creating the project.'; var msg = data.message || 'An error occurred creating the project.';
$scope.alerts['error-creating-project'] = {type: 'error', message: msg}; showAlert('error-creating-project', {type: 'error', message: msg});
} }
}); });
} }
......
'use strict'; 'use strict';
angular.module("openshiftCommonUI") angular.module("openshiftCommonUI")
.directive("deleteProject", function ($uibModal, $location, $filter, $q, hashSizeFilter, APIService, DataService, AlertMessageService, Logger) { .directive("deleteProject", function ($uibModal, $location, $filter, $q, hashSizeFilter, APIService, DataService, AlertMessageService, NotificationsService, Logger) {
return { return {
restrict: "E", restrict: "E",
scope: { scope: {
// The name of project to delete // The name of project to delete
projectName: "@", projectName: "@",
// Alerts object for success and error alerts. // Alerts object for using inline notifications for success and error alerts, notifications are also sent to enable toast notification display.
alerts: "=", alerts: "=",
// Optional display name of the project to delete. // Optional display name of the project to delete.
displayName: "@", displayName: "@",
...@@ -42,6 +42,7 @@ angular.module("openshiftCommonUI") ...@@ -42,6 +42,7 @@ angular.module("openshiftCommonUI")
} else { } else {
AlertMessageService.addAlert(alert); AlertMessageService.addAlert(alert);
} }
NotificationsService.addNotification(alert.data);
}; };
var navigateToList = function() { var navigateToList = function() {
...@@ -102,11 +103,13 @@ angular.module("openshiftCommonUI") ...@@ -102,11 +103,13 @@ angular.module("openshiftCommonUI")
}) })
.catch(function(err) { .catch(function(err) {
// called if failure to delete // called if failure to delete
scope.alerts[projectName] = { var alert = {
type: "error", type: "error",
message: _.capitalize(formattedResource) + "\'" + " could not be deleted.", message: _.capitalize(formattedResource) + "\'" + " could not be deleted.",
details: $filter('getErrorDetails')(err) details: $filter('getErrorDetails')(err)
}; };
scope.alerts[projectName] = alert;
NotificationsService.addNotification(alert);
Logger.error(formattedResource + " could not be deleted.", err); Logger.error(formattedResource + " could not be deleted.", err);
}); });
}); });
......
...@@ -14,7 +14,7 @@ angular.module("openshiftCommonUI") ...@@ -14,7 +14,7 @@ angular.module("openshiftCommonUI")
isDialog: '@' isDialog: '@'
}, },
templateUrl: 'src/components/edit-project/editProject.html', templateUrl: 'src/components/edit-project/editProject.html',
controller: function($scope, $filter, $location, DataService, annotationNameFilter) { controller: function($scope, $filter, $location, DataService, NotificationsService, annotationNameFilter, displayNameFilter) {
if(!($scope.submitButtonLabel)) { if(!($scope.submitButtonLabel)) {
$scope.submitButtonLabel = 'Save'; $scope.submitButtonLabel = 'Save';
} }
...@@ -51,6 +51,11 @@ angular.module("openshiftCommonUI") ...@@ -51,6 +51,11 @@ angular.module("openshiftCommonUI")
return resource; return resource;
}; };
var showAlert = function(alert) {
$scope.alerts["update"] = alert;
NotificationsService.addNotification(alert);
};
$scope.editableFields = editableFields($scope.project); $scope.editableFields = editableFields($scope.project);
$scope.update = function() { $scope.update = function() {
...@@ -63,20 +68,25 @@ angular.module("openshiftCommonUI") ...@@ -63,20 +68,25 @@ angular.module("openshiftCommonUI")
cleanEditableAnnotations(mergeEditable($scope.project, $scope.editableFields)), cleanEditableAnnotations(mergeEditable($scope.project, $scope.editableFields)),
{projectName: $scope.project.name}, {projectName: $scope.project.name},
{errorNotification: false}) {errorNotification: false})
.then(function() { .then(function(project) {
// angular is actually wrapping the redirect action :/ // angular is actually wrapping the redirect action :/
var cb = $scope.redirectAction(); var cb = $scope.redirectAction();
if (cb) { if (cb) {
cb(encodeURIComponent($scope.project.metadata.name)); cb(encodeURIComponent($scope.project.metadata.name));
} }
showAlert({
type: "success",
message: "Project \'" + displayNameFilter(project) + "\' was successfully updated."
});
}, function(result) { }, function(result) {
$scope.disableInputs = false; $scope.disableInputs = false;
$scope.editableFields = editableFields($scope.project); $scope.editableFields = editableFields($scope.project);
$scope.alerts["update"] = { showAlert({
type: "error", type: "error",
message: "An error occurred while updating the project", message: "An error occurred while updating the project",
details: $filter('getErrorDetails')(result) details: $filter('getErrorDetails')(result)
}; });
}); });
} }
}; };
......
<div class="toast-notifications-list-pf">
<div ng-repeat="(notificationID, notification) in notifications track by (notificationID + (notification.message || notification.details))" ng-if="!notification.hidden"
ng-mouseenter="setHover(notification, true)" ng-mouseleave="setHover(notification, false)">
<div class="toast-pf alert {{notification.type | alertStatus}}" ng-class="{'alert-dismissable': !hideCloseButton}">
<button ng-if="!hideCloseButton" type="button" class="close" ng-click="close(notification)">
<span class="pficon pficon-close" aria-hidden="true"></span>
<span class="sr-only">Close</span>
</button>
<span class="{{notification.type | alertIcon}}" aria-hidden="true"></span>
<span class="sr-only">{{notification.type}}</span>
<span class="toast-notification-message" ng-if="notification.message">{{notification.message}}</span>
<span ng-if="notification.details">{{notification.details}}</span>
<span ng-repeat="link in notification.links">
<a ng-if="!link.href" href="" ng-click="onClick(notification, link)" role="button">{{link.label}}</a>
<a ng-if="link.href" ng-href="{{link.href}}" ng-attr-target="{{link.target}}">{{link.label}}</a>
<span ng-if="!$last" class="toast-action-divider">|</span>
</span>
</div>
</div>
</div>
'use strict';
angular.module('openshiftCommonUI')
.directive('toastNotifications', function(NotificationsService, $timeout) {
return {
restrict: 'E',
scope: {},
templateUrl: 'src/components/toast-notifications/toast-notifications.html',
link: function($scope) {
$scope.notifications = NotificationsService.getNotifications();
$scope.close = function(notification) {
notification.hidden = true;
if (_.isFunction(notification.onClose)) {
notification.onClose();
}
};
$scope.onClick = function(notification, link) {
if (_.isFunction(link.onClick)) {
// If onClick() returns true, also hide the alert.
var close = link.onClick();
if (close) {
notification.hidden = true;
}
}
};
$scope.setHover = function(notification, isHover) {
notification.isHover = isHover;
};
$scope.$watch('notifications', function() {
_.each($scope.notifications, function(notification) {
if (NotificationsService.isAutoDismiss(notification) && !notification.hidden) {
if (!notification.timerId) {
notification.timerId = $timeout(function () {
notification.timerId = -1;
if (!notification.isHover) {
notification.hidden = true;
}
}, NotificationsService.dismissDelay);
} else if (notification.timerId === -1 && !notification.isHover) {
notification.hidden = true;
}
}
});
}, true);
}
};
});
'use strict';
angular.module('openshiftCommonUI')
.filter("alertStatus", function() {
return function (type) {
var status;
switch(type) {
case 'error':
status = 'alert-danger';
break;
case 'warning':
status = 'alert-warning';
break;
case 'success':
status = 'alert-success';
break;
default:
status = 'alert-info';
}
return status;
};
})
.filter('alertIcon', function() {
return function (type) {
var icon;
switch(type) {
case 'error':
icon = 'pficon pficon-error-circle-o';
break;
case 'warning':
icon = 'pficon pficon-warning-triangle-o';
break;
case 'success':
icon = 'pficon pficon-ok';
break;
default:
icon = 'pficon pficon-info';
}
return icon;
};
});
...@@ -15,6 +15,16 @@ angular.module('openshiftCommonUI') ...@@ -15,6 +15,16 @@ angular.module('openshiftCommonUI')
return Object.keys(hash).length; return Object.keys(hash).length;
}; };
}) })
// Wraps _.filter. Works with hashes, unlike ngFilter, which only works
// with arrays.
.filter('filterCollection', function() {
return function(collection, predicate) {
if (!collection || !predicate) {
return collection;
}
return _.filter(collection, predicate);
};
})
.filter('generateName', function() { .filter('generateName', function() {
return function(prefix, length) { return function(prefix, length) {
if (!prefix) { if (!prefix) {
...@@ -26,4 +36,19 @@ angular.module('openshiftCommonUI') ...@@ -26,4 +36,19 @@ angular.module('openshiftCommonUI')
var randomString = Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1); var randomString = Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1);
return prefix + randomString; return prefix + randomString;
}; };
})
.filter("getErrorDetails", function(upperFirstFilter) {
return function(result, capitalize) {
var error = result.data || {};
if (error.message) {
return capitalize ? upperFirstFilter(error.message) : error.message;
}
var status = result.status || error.status;
if (status) {
return "Status: " + status;
}
return "";
};
}); });
.toast-action-divider {
color: @gray-light;
}
.toast-notification-message {
font-weight: 700;
margin-right: 5px;
}
...@@ -14,4 +14,5 @@ ...@@ -14,4 +14,5 @@
@import "_forms.less"; @import "_forms.less";
@import "_guided-tour.less"; @import "_guided-tour.less";
@import "_messages.less"; @import "_messages.less";
@import "_notifications.less";
@import "_ui-select.less"; @import "_ui-select.less";
'use strict';
angular.module('openshiftCommonServices').provider('NotificationsService', function() {
this.dismissDelay = 8000;
this.autoDismissTypes = ['info', 'success'];
this.$get = function() {
var notifications = [];
var dismissDelay = this.dismissDelay;
var autoDismissTypes = this.autoDismissTypes;
var notificationHiddenKey = function(notificationID, namespace) {
if (!namespace) {
return 'hide/notification/' + notificationID;
}
return 'hide/notification/' + namespace + '/' + notificationID;
};
var addNotification = function (notification, notificationID, namespace) {
if (notificationID && isNotificationPermanentlyHidden(notificationID, namespace)) {
notification.hidden = true;
}
notifications.push(notification);
};
var getNotifications = function () {
return notifications;
};
var clearNotifications = function () {
_.take(notifications, 0);
};
var isNotificationPermanentlyHidden = function (notificationID, namespace) {
var key = notificationHiddenKey(notificationID, namespace);
return localStorage.getItem(key) === 'true';
};
var permanentlyHideNotification = function (notificationID, namespace) {
var key = notificationHiddenKey(notificationID, namespace);
localStorage.setItem(key, 'true');
};
var isAutoDismiss = function(notification) {
return _.find(autoDismissTypes, function(type) {
return type === notification.type;
});
};
return {
addNotification: addNotification,
getNotifications: getNotifications,
clearNotifications: clearNotifications,
isNotificationPermanentlyHidden: isNotificationPermanentlyHidden,
permanentlyHideNotification: permanentlyHideNotification,
isAutoDismiss: isAutoDismiss,
dismissDelay: dismissDelay,
autoDismissTypes: autoDismissTypes
};
};
this.setDismissDelay = function(delayInMs) {
this.dismissDelay = delayInMs;
};
this.setAutoDismissTypes = function(arrayOfTypes) {
this.autoDismissTypes = arrayOfTypes;
};
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment