You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2021/03/18 23:18:21 UTC
[trafficcontrol] branch master updated: Improves cdn notifications
functionality (#5643)
This is an automated email from the ASF dual-hosted git repository.
ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new 6959ec6 Improves cdn notifications functionality (#5643)
6959ec6 is described below
commit 6959ec6ae29ccc403ce288f44d0f1c3decaaa4bd
Author: Jeremy Mitchell <mi...@users.noreply.github.com>
AuthorDate: Thu Mar 18 17:18:08 2021 -0600
Improves cdn notifications functionality (#5643)
* creates a drop down for cdn notifications
* adds cdn notifications tables and provides access to them
* adds the ability to create notifications
* adds ability to delete notifications
* show the ALL cdn in the header bar
* getting order or cdns and notifications right
* allows the creation of multiple notifications for a cdn
* fixes cdn notification tests
* allows the user to dismiss notifications and localstorage will remember that dismissal
* fixes goose down to put the cdn_notification table back like it was before
* makes notification a required property
* fixes formatting
* uses ng-if rather than ng-show
* changes anchor to button
---
docs/source/api/v4/cdn_notifications.rst | 14 +-
lib/go-tc/cdn_notification.go | 6 +-
...2021031400000000_cdn_notifications_multiple.sql | 50 +++
.../testing/api/v4/cdnnotifications_test.go | 16 +-
.../cdnnotification/cdnnotifications.go | 38 ++-
traffic_ops/v4-client/cdn_notifications.go | 7 +-
traffic_portal/app/src/app.js | 5 +
.../common/modules/form/cdn/FormCDNController.js | 4 +
.../src/common/modules/form/cdn/form.cdn.tpl.html | 2 +
.../src/common/modules/header/HeaderController.js | 80 +----
.../app/src/common/modules/header/header.tpl.html | 28 +-
.../common/modules/navigation/navigation.tpl.html | 1 +
.../notifications/NotificationsController.js | 19 +-
.../modules/notifications/notifications.tpl.html | 3 +-
.../TableCDNNotificationsController.js | 29 ++
.../common/modules/table/cdnNotifications/index.js | 21 ++
.../table.cdnNotifications.tpl.html | 76 +++++
.../modules/table/cdns/TableCDNsController.js | 6 +
.../notifications/TableNotificationsController.js | 345 +++++++++++++++++++++
.../common/modules/table/notifications/index.js | 21 ++
.../notifications/table.notifications.tpl.html | 74 +++++
.../modules/private/cdns/notifications/index.js | 51 +++
.../app/src/modules/private/notifications/index.js | 34 ++
.../modules/private/notifications/list/index.js | 45 +++
.../private}/notifications/notifications.tpl.html | 6 +-
25 files changed, 865 insertions(+), 116 deletions(-)
diff --git a/docs/source/api/v4/cdn_notifications.rst b/docs/source/api/v4/cdn_notifications.rst
index 1da1de4..bdad91f 100644
--- a/docs/source/api/v4/cdn_notifications.rst
+++ b/docs/source/api/v4/cdn_notifications.rst
@@ -35,9 +35,11 @@ Request Structure
+------------+----------+-----------------------------------------------------------------------------------------------------+
| Parameter | Required | Description |
+============+==========+=====================================================================================================+
- | cdn | no | The CDN name of the notification you wish to retrieve. |
+ | cdn | no | The CDN name of the notifications you wish to retrieve. |
+------------+----------+-----------------------------------------------------------------------------------------------------+
- | user | no | The username of the user responsible for creating the CDN notification. |
+ | id | no | The integral, unique identifier of the notification you wish to retrieve. |
+ +------------+----------+-----------------------------------------------------------------------------------------------------+
+ | user | no | The username of the user responsible for creating the CDN notifications. |
+------------+----------+-----------------------------------------------------------------------------------------------------+
.. code-block:: http
@@ -52,6 +54,7 @@ Request Structure
Response Structure
------------------
+:id: The integral, unique identifier of the notification
:cdn: The name of the CDN to which the notification belongs to
:lastUpdated: The time and date this server entry was last updated in an ISO-like format
:notification: The content of the notification
@@ -75,6 +78,7 @@ Response Structure
{ "response": [
{
+ "id": 42,
"cdn": "cdn1",
"lastUpdated": "2019-12-02 21:49:08+00",
"notification": "the content of the notification",
@@ -113,6 +117,7 @@ Request Structure
Response Structure
------------------
+:id: The integral, unique identifier of the notification
:cdn: The name of the CDN to which the notification belongs to
:lastUpdated: The time and date this server entry was last updated in an ISO-like format
:notification: The content of the notification
@@ -144,6 +149,7 @@ Response Structure
],
"response":
{
+ "id": 42,
"cdn": "cdn1",
"lastUpdated": "2019-12-02 21:49:08+00",
"notification": "the content of the notification",
@@ -166,13 +172,13 @@ Request Structure
+------------+----------+-----------------------------------------------------------------------------------------------------+
| Parameter | Required | Description |
+============+==========+=====================================================================================================+
- | cdn | yes | The CDN name of the notification you wish to delete. |
+ | id | yes | The integral, unique identifier of the notification you wish to delete. |
+------------+----------+-----------------------------------------------------------------------------------------------------+
.. code-block:: http
:caption: Request Example
- DELETE /api/4.0/cdn_notifications?cdn=cdn1 HTTP/1.1
+ DELETE /api/4.0/cdn_notifications?id=42 HTTP/1.1
User-Agent: python-requests/2.22.0
Accept-Encoding: gzip, deflate
Accept: */*
diff --git a/lib/go-tc/cdn_notification.go b/lib/go-tc/cdn_notification.go
index 30d418d..f922e81 100644
--- a/lib/go-tc/cdn_notification.go
+++ b/lib/go-tc/cdn_notification.go
@@ -44,16 +44,18 @@ type CDNNotificationRequest struct {
// CDNNotification is a notification created for a specific CDN.
type CDNNotification struct {
+ ID int `json:"id" db:"id"`
CDN string `json:"cdn" db:"cdn"`
LastUpdated time.Time `json:"lastUpdated" db:"last_updated"`
- Notification *string `json:"notification" db:"notification"`
+ Notification string `json:"notification" db:"notification"`
User string `json:"user" db:"user"`
}
// Validate validates the CDNNotificationRequest request is valid for creation.
func (n *CDNNotificationRequest) Validate(tx *sql.Tx) error {
errs := validation.Errors{
- "cdn": validation.Validate(n.CDN, validation.Required),
+ "cdn": validation.Validate(n.CDN, validation.Required),
+ "notification": validation.Validate(n.Notification, validation.Required),
}
return util.JoinErrs(tovalidate.ToErrors(errs))
}
diff --git a/traffic_ops/app/db/migrations/2021031400000000_cdn_notifications_multiple.sql b/traffic_ops/app/db/migrations/2021031400000000_cdn_notifications_multiple.sql
new file mode 100644
index 0000000..5347bb9
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2021031400000000_cdn_notifications_multiple.sql
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+-- +goose Up
+-- SQL in section 'Up' is executed when this migration is applied
+DROP TABLE IF EXISTS cdn_notification;
+
+CREATE TABLE cdn_notification (
+ id BIGSERIAL PRIMARY KEY,
+ cdn text NOT NULL,
+ "user" text NOT NULL,
+ notification text NOT NULL,
+ last_updated timestamp with time zone DEFAULT now() NOT NULL,
+ CONSTRAINT fk_notification_cdn FOREIGN KEY (cdn) REFERENCES cdn(name) ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT fk_notification_user FOREIGN KEY ("user") REFERENCES tm_user(username) ON DELETE CASCADE ON UPDATE CASCADE
+);
+DROP TRIGGER IF EXISTS on_update_current_timestamp ON cdn_notification;
+CREATE TRIGGER on_update_current_timestamp BEFORE UPDATE ON cdn_notification FOR EACH ROW EXECUTE PROCEDURE on_update_current_timestamp_last_updated();
+
+
+-- +goose Down
+-- SQL section 'Down' is executed when this migration is rolled back
+DROP TABLE IF EXISTS cdn_notification;
+
+CREATE TABLE cdn_notification (
+ cdn text NOT NULL,
+ "user" text NOT NULL,
+ notification text,
+ last_updated timestamp with time zone DEFAULT now() NOT NULL,
+ CONSTRAINT pk_cdn_notification PRIMARY KEY (cdn),
+ CONSTRAINT fk_notification_cdn FOREIGN KEY (cdn) REFERENCES cdn(name) ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT fk_notification_user FOREIGN KEY ("user") REFERENCES tm_user(username) ON DELETE CASCADE ON UPDATE CASCADE
+);
+DROP TRIGGER IF EXISTS on_update_current_timestamp ON cdn_notification;
+CREATE TRIGGER on_update_current_timestamp BEFORE UPDATE ON cdn_notification FOR EACH ROW EXECUTE PROCEDURE on_update_current_timestamp_last_updated();
+
diff --git a/traffic_ops/testing/api/v4/cdnnotifications_test.go b/traffic_ops/testing/api/v4/cdnnotifications_test.go
index 80ae9d4..13f0a05 100644
--- a/traffic_ops/testing/api/v4/cdnnotifications_test.go
+++ b/traffic_ops/testing/api/v4/cdnnotifications_test.go
@@ -36,8 +36,8 @@ func GetTestCDNotifications(t *testing.T) {
if len(resp) > 0 {
respNotification := resp[0]
expectedNotification := "test notification: " + cdn.Name
- if respNotification.Notification != nil && *respNotification.Notification != expectedNotification {
- t.Errorf("expected notification does not match actual: %s, expected: %s", *respNotification.Notification, expectedNotification)
+ if respNotification.Notification != expectedNotification {
+ t.Errorf("expected notification does not match actual: %s, expected: %s", respNotification.Notification, expectedNotification)
}
}
}
@@ -54,9 +54,17 @@ func CreateTestCDNNotifications(t *testing.T) {
func DeleteTestCDNNotifications(t *testing.T) {
for _, cdn := range testData.CDNs {
- _, _, err := TOSession.DeleteCDNNotification(cdn.Name)
+ // Retrieve the notifications for a cdn
+ resp, _, err := TOSession.GetCDNNotifications(cdn.Name, nil)
if err != nil {
- t.Errorf("cannot DELETE CDN notification: '%s' %v", cdn.Name, err)
+ t.Errorf("cannot GET notifications for a CDN: %v - %v", cdn.Name, err)
+ }
+ if len(resp) > 0 {
+ respNotification := resp[0]
+ _, _, err := TOSession.DeleteCDNNotification(respNotification.ID)
+ if err != nil {
+ t.Errorf("cannot DELETE CDN notification by ID: '%d' %v", respNotification.ID, err)
+ }
}
}
}
diff --git a/traffic_ops/traffic_ops_golang/cdnnotification/cdnnotifications.go b/traffic_ops/traffic_ops_golang/cdnnotification/cdnnotifications.go
index 60d1214..66c36d4 100644
--- a/traffic_ops/traffic_ops_golang/cdnnotification/cdnnotifications.go
+++ b/traffic_ops/traffic_ops_golang/cdnnotification/cdnnotifications.go
@@ -32,7 +32,8 @@ import (
)
const readQuery = `
-SELECT cn.cdn,
+SELECT cn.id,
+ cn.cdn,
cn.last_updated,
cn.user,
cn.notification
@@ -44,19 +45,21 @@ INNER JOIN tm_user ON tm_user.username = cn.user
const insertQuery = `
INSERT INTO cdn_notification (cdn, "user", notification)
VALUES ($1, $2, $3)
-RETURNING cdn_notification.cdn,
- cdn_notification.last_updated,
- cdn_notification.user,
- cdn_notification.notification
+RETURNING cdn_notification.id,
+cdn_notification.cdn,
+cdn_notification.last_updated,
+cdn_notification.user,
+cdn_notification.notification
`
const deleteQuery = `
DELETE FROM cdn_notification
-WHERE cdn_notification.cdn = $1
-RETURNING cdn_notification.cdn,
- cdn_notification.last_updated,
- cdn_notification.user,
- cdn_notification.notification
+WHERE cdn_notification.id = $1
+RETURNING cdn_notification.id,
+cdn_notification.cdn,
+cdn_notification.last_updated,
+cdn_notification.user,
+cdn_notification.notification
`
// Read is the handler for GET requests to /cdn_notifications.
@@ -72,6 +75,7 @@ func Read(w http.ResponseWriter, r *http.Request) {
cdnNotifications := []tc.CDNNotification{}
queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{
+ "id": dbhelpers.WhereColumnInfo{"cn.id", api.IsInt},
"cdn": dbhelpers.WhereColumnInfo{"cdn.name", nil},
"user": dbhelpers.WhereColumnInfo{"tm_user.username", nil},
}
@@ -99,7 +103,7 @@ func Read(w http.ResponseWriter, r *http.Request) {
for rows.Next() {
var n tc.CDNNotification
- if err = rows.Scan(&n.CDN, &n.LastUpdated, &n.User, &n.Notification); err != nil {
+ if err = rows.Scan(&n.ID, &n.CDN, &n.LastUpdated, &n.User, &n.Notification); err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, errors.New("scanning cdn notifications: "+err.Error()))
return
}
@@ -126,14 +130,14 @@ func Create(w http.ResponseWriter, r *http.Request) {
}
var resp tc.CDNNotification
- err := tx.QueryRow(insertQuery, req.CDN, inf.User.UserName, req.Notification).Scan(&resp.CDN, &resp.LastUpdated, &resp.User, &resp.Notification)
+ err := tx.QueryRow(insertQuery, req.CDN, inf.User.UserName, req.Notification).Scan(&resp.ID, &resp.CDN, &resp.LastUpdated, &resp.User, &resp.Notification)
if err != nil {
userErr, sysErr, errCode = api.ParseDBError(err)
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
- changeLogMsg := fmt.Sprintf("CDN_NOTIFICATION: %s, CDN: %s, ACTION: Created", *resp.Notification, resp.CDN)
+ changeLogMsg := fmt.Sprintf("CDN_NOTIFICATION: %s, CDN: %s, ACTION: Created", resp.Notification, resp.CDN)
api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
alertMsg := fmt.Sprintf("CDN notification created [ User = %s ] for CDN: %s", resp.User, resp.CDN)
@@ -143,7 +147,7 @@ func Create(w http.ResponseWriter, r *http.Request) {
// Delete is the handler for DELETE requests to /cdn_notifications.
func Delete(w http.ResponseWriter, r *http.Request) {
- inf, sysErr, userErr, errCode := api.NewInfo(r, []string{"cdn"}, nil)
+ inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
tx := inf.Tx.Tx
if sysErr != nil || userErr != nil {
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
@@ -167,10 +171,10 @@ func deleteCDNNotification(inf *api.APIInfo) (tc.Alert, tc.CDNNotification, erro
var alert tc.Alert
var result tc.CDNNotification
- err := inf.Tx.Tx.QueryRow(deleteQuery, inf.Params["cdn"]).Scan(&result.CDN, &result.LastUpdated, &result.User, &result.Notification)
+ err := inf.Tx.Tx.QueryRow(deleteQuery, inf.Params["id"]).Scan(&result.ID, &result.CDN, &result.LastUpdated, &result.User, &result.Notification)
if err != nil {
if err == sql.ErrNoRows {
- userErr = fmt.Errorf("No CDN Notification for %s", inf.Params["cdn"])
+ userErr = fmt.Errorf("No CDN Notification for %s", inf.Params["id"])
statusCode = http.StatusNotFound
} else {
userErr, sysErr, statusCode = api.ParseDBError(err)
@@ -179,7 +183,7 @@ func deleteCDNNotification(inf *api.APIInfo) (tc.Alert, tc.CDNNotification, erro
return alert, result, userErr, sysErr, statusCode
}
- changeLogMsg := fmt.Sprintf("CDN_NOTIFICATION: %s, CDN: %s, ACTION: Deleted", *result.Notification, result.CDN)
+ changeLogMsg := fmt.Sprintf("CDN_NOTIFICATION: %s, CDN: %s, ACTION: Deleted", result.Notification, result.CDN)
api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, inf.Tx.Tx)
alertMsg := fmt.Sprintf("CDN notification deleted [ User = %s ] for CDN: %s", result.User, result.CDN)
diff --git a/traffic_ops/v4-client/cdn_notifications.go b/traffic_ops/v4-client/cdn_notifications.go
index 8925668..7cd563c 100644
--- a/traffic_ops/v4-client/cdn_notifications.go
+++ b/traffic_ops/v4-client/cdn_notifications.go
@@ -19,6 +19,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "strconv"
"github.com/apache/trafficcontrol/lib/go-tc"
"github.com/apache/trafficcontrol/traffic_ops/toclientlib"
@@ -45,11 +46,11 @@ func (to *Session) CreateCDNNotification(notification tc.CDNNotificationRequest)
return alerts, reqInf, err
}
-// DeleteCDNNotification deletes a CDN Notification by CDN name.
-func (to *Session) DeleteCDNNotification(cdnName string) (tc.Alerts, toclientlib.ReqInf, error) {
+// DeleteCDNNotification deletes a CDN Notification by notification ID.
+func (to *Session) DeleteCDNNotification(id int) (tc.Alerts, toclientlib.ReqInf, error) {
var alerts tc.Alerts
params := url.Values{}
- params.Add("cdn", cdnName)
+ params.Add("id", strconv.Itoa(id))
route := fmt.Sprintf("%s?%s", APICDNNotifications, params.Encode())
reqInf, err := to.del(route, nil, &alerts)
return alerts, reqInf, err
diff --git a/traffic_portal/app/src/app.js b/traffic_portal/app/src/app.js
index 0e47f22..fd624d0 100644
--- a/traffic_portal/app/src/app.js
+++ b/traffic_portal/app/src/app.js
@@ -88,6 +88,7 @@ var trafficPortal = angular.module('trafficPortal', [
require('./modules/private/cdns/federations/users').name,
require('./modules/private/cdns/list').name,
require('./modules/private/cdns/new').name,
+ require('./modules/private/cdns/notifications').name,
require('./modules/private/cdns/profiles').name,
require('./modules/private/cdns/servers').name,
require('./modules/private/changeLogs').name,
@@ -147,6 +148,8 @@ var trafficPortal = angular.module('trafficPortal', [
require('./modules/private/jobs').name,
require('./modules/private/jobs/list').name,
require('./modules/private/jobs/new').name,
+ require('./modules/private/notifications').name,
+ require('./modules/private/notifications/list').name,
require('./modules/private/origins').name,
require('./modules/private/origins/edit').name,
require('./modules/private/origins/list').name,
@@ -366,6 +369,7 @@ var trafficPortal = angular.module('trafficPortal', [
require('./common/modules/table/cdnFederations').name,
require('./common/modules/table/cdnFederationDeliveryServices').name,
require('./common/modules/table/cdnFederationUsers').name,
+ require('./common/modules/table/cdnNotifications').name,
require('./common/modules/table/cdnProfiles').name,
require('./common/modules/table/cdnServers').name,
require('./common/modules/table/coordinates').name,
@@ -385,6 +389,7 @@ var trafficPortal = angular.module('trafficPortal', [
require('./common/modules/table/endpoints').name,
require('./common/modules/table/federationResolvers').name,
require('./common/modules/table/jobs').name,
+ require('./common/modules/table/notifications').name,
require('./common/modules/table/origins').name,
require('./common/modules/table/physLocations').name,
require('./common/modules/table/physLocationServers').name,
diff --git a/traffic_portal/app/src/common/modules/form/cdn/FormCDNController.js b/traffic_portal/app/src/common/modules/form/cdn/FormCDNController.js
index f495032..fe19845 100644
--- a/traffic_portal/app/src/common/modules/form/cdn/FormCDNController.js
+++ b/traffic_portal/app/src/common/modules/form/cdn/FormCDNController.js
@@ -58,6 +58,10 @@ var FormCDNController = function(cdn, $scope, $location, $uibModal, formUtils, s
$location.path($location.path() + '/delivery-services');
};
+ $scope.viewNotifications = function() {
+ $location.path($location.path() + '/notifications');
+ };
+
$scope.queueServerUpdates = function(cdn) {
var params = {
title: 'Queue Server Updates: ' + cdn.name,
diff --git a/traffic_portal/app/src/common/modules/form/cdn/form.cdn.tpl.html b/traffic_portal/app/src/common/modules/form/cdn/form.cdn.tpl.html
index 21ae568..534f591 100644
--- a/traffic_portal/app/src/common/modules/form/cdn/form.cdn.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/cdn/form.cdn.tpl.html
@@ -47,6 +47,8 @@ under the License.
<li role="menuitem"><a ng-click="viewDeliveryServices()">View Delivery Services</a></li>
<li role="menuitem"><a ng-click="viewProfiles()">View Profiles</a></li>
<li role="menuitem"><a ng-click="viewServers()">View Servers</a></li>
+ <li class="divider"></li>
+ <li role="menuitem"><a ng-click="viewNotifications()">View Notifications</a></li>
</ul>
</div>
</div>
diff --git a/traffic_portal/app/src/common/modules/header/HeaderController.js b/traffic_portal/app/src/common/modules/header/HeaderController.js
index 8ca0666..9dec203 100644
--- a/traffic_portal/app/src/common/modules/header/HeaderController.js
+++ b/traffic_portal/app/src/common/modules/header/HeaderController.js
@@ -20,14 +20,10 @@
var HeaderController = function($rootScope, $scope, $state, $uibModal, $location, $anchorScroll, locationUtils, permissionUtils, authService, trafficPortalService, changeLogService, cdnService, changeLogModel, userModel, propertiesModel) {
let getCDNs = function(notifications) {
- cdnService.getCDNs()
+ cdnService.getCDNs(true)
.then(function(cdns) {
cdns.forEach(function(cdn) {
- const cdnNotification = notifications.find(function(notification){ return cdn.name === notification.cdn });
- if (cdnNotification) {
- cdn.notificationCreatedBy = cdnNotification.user;
- cdn.notification = cdnNotification.notification;
- }
+ cdn.hasNotifications = notifications.find(function(notification){ return cdn.name === notification.cdn });
});
$scope.cdns = cdns;
});
@@ -66,6 +62,16 @@ var HeaderController = function($rootScope, $scope, $state, $uibModal, $location
});
};
+ $scope.getNotifications = function(cdn) {
+ $scope.loadingNotifications = true;
+ $scope.notifications = [];
+ cdnService.getNotifications({ cdn: cdn.name })
+ .then(function(response) {
+ $scope.loadingNotifications = false;
+ $scope.notifications = response;
+ });
+ };
+
$scope.getRelativeTime = function(date) {
return moment(date).fromNow();
};
@@ -78,68 +84,6 @@ var HeaderController = function($rootScope, $scope, $state, $uibModal, $location
trafficPortalService.dbDump();
};
- $scope.toggleNotification = function(cdn) {
- if (cdn.notificationCreatedBy) {
- confirmDeleteNotification(cdn);
- } else {
- confirmCreateNotification(cdn);
- }
- };
-
- let confirmCreateNotification = function(cdn) {
- const params = {
- title: 'Create Global ' + cdn.name + ' Notification',
- message: 'What is the content of your global notification for the ' + cdn.name + ' CDN?'
- };
- const modalInstance = $uibModal.open({
- templateUrl: 'common/modules/dialog/input/dialog.input.tpl.html',
- controller: 'DialogInputController',
- size: 'md',
- resolve: {
- params: function () {
- return params;
- }
- }
- });
- modalInstance.result.then(function(notification) {
- cdnService.createNotification(cdn, notification).
- then(
- function() {
- $rootScope.$broadcast('headerController::notificationCreated');
- }
- );
- }, function () {
- // do nothing
- });
- };
-
- let confirmDeleteNotification = function(cdn) {
- const params = {
- title: 'Delete Global ' + cdn.name + ' Notification',
- message: 'Are you sure you want to delete the global notification for the ' + cdn.name + ' CDN? This will remove the notification from the view of all users.'
- };
- const modalInstance = $uibModal.open({
- templateUrl: 'common/modules/dialog/confirm/dialog.confirm.tpl.html',
- controller: 'DialogConfirmController',
- size: 'md',
- resolve: {
- params: function () {
- return params;
- }
- }
- });
- modalInstance.result.then(function() {
- cdnService.deleteNotification({ cdn: cdn.name }).
- then(
- function() {
- $rootScope.$broadcast('headerController::notificationDeleted');
- }
- );
- }, function () {
- // do nothing
- });
- };
-
$scope.confirmQueueServerUpdates = function() {
var params = {
title: 'Queue Server Updates',
diff --git a/traffic_portal/app/src/common/modules/header/header.tpl.html b/traffic_portal/app/src/common/modules/header/header.tpl.html
index 004e249..0c63706 100644
--- a/traffic_portal/app/src/common/modules/header/header.tpl.html
+++ b/traffic_portal/app/src/common/modules/header/header.tpl.html
@@ -80,9 +80,31 @@ under the License.
<button type="button" class="btn btn-link" ng-if="hasCapability('cdns-snapshot')" ng-click="snapshot()"><i class="fa fa-camera"></i></button>
</div>
</li>
- <li ng-repeat="cdn in cdns">
- <div class="btn-group" title="{{(cdn.notificationCreatedBy) ? 'Delete Global ' + cdn.name + ' Notification' : 'Create Global ' + cdn.name + ' Notification'}}">
- <button type="button" class="notification btn btn-link" ng-click="toggleNotification(cdn)">{{cdn.name}} <i class="fa" ng-class="{ 'fa-bell': cdn.notificationCreatedBy, 'fa-bell-o': !cdn.notificationCreatedBy }"></i> <i ng-show="cdn.notificationCreatedBy" class="fa fa-times"></i></button>
+ <li role="presentation" class="dropdown" ng-repeat="cdn in cdns | orderBy: '-name'">
+ <div class="btn-group" title="{{cdn.name}} Notifications" uib-dropdown is-open="{{cdn.name}}-notifications.isopen">
+ <button id="{{cdn.name}}-notifications" type="button" class="btn btn-link" ng-click="getNotifications(cdn)" uib-dropdown-toggle>
+ {{cdn.name}} <i class="fa" ng-class="{ 'fa-bell': cdn.hasNotifications, 'fa-bell-o': !cdn.hasNotifications }"></i>
+ </button>
+ <ul class="uib-dropdown-menu list-unstyled msg_list animated fadeInDown" role="menu">
+ <li ng-if="loadingNotifications" style="text-align: center"><i class="fa fa-refresh fa-spin fa-1x fa-fw"></i></li>
+ <li ng-if="notifications.length === 0">
+ <div class="text-center">No {{cdn.name}} notifications</div>
+ </li>
+ <li ng-repeat="notification in notifications">
+ <a ng-click="$event.stopPropagation()">
+ <span>
+ <span>{{::notification.user}}</span>
+ <span class="time">{{::getRelativeTime(notification.lastUpdated)}}</span>
+ </span>
+ <span class="message">{{::notification.notification}}</span>
+ </a>
+ </li>
+ <li>
+ <div class="text-center">
+ <button type="button" class="btn btn-link" title="Manage {{cdn.name}} notifications" ng-click="navigateToPath('/cdns/' + cdn.id + '/notifications')"><strong>Manage {{cdn.name}} notifications <i class="fa fa-angle-right"></i></strong></button>
+ </div>
+ </li>
+ </ul>
</div>
</li>
</ul>
diff --git a/traffic_portal/app/src/common/modules/navigation/navigation.tpl.html b/traffic_portal/app/src/common/modules/navigation/navigation.tpl.html
index d9c81d3..bf067e9 100644
--- a/traffic_portal/app/src/common/modules/navigation/navigation.tpl.html
+++ b/traffic_portal/app/src/common/modules/navigation/navigation.tpl.html
@@ -73,6 +73,7 @@ under the License.
<li class="side-menu-category-item" ng-if="hasCapability('users-read')" ng-class="{'current-page': isState('trafficPortal.private.users')}"><a href="/#!/users">Users</a></li>
<li class="side-menu-category-item" ng-if="hasCapability('tenants-read')" ng-class="{'current-page': isState('trafficPortal.private.tenants')}"><a href="/#!/tenants">Tenants</a></li>
<li class="side-menu-category-item" ng-if="hasCapability('roles-read')" ng-class="{'current-page': isState('trafficPortal.private.roles')}"><a href="/#!/roles">Roles</a></li>
+ <li class="side-menu-category-item" ng-if="hasCapability('notifications-read')" ng-class="{'current-page': isState('trafficPortal.private.notifications')}"><a href="/#!/notifications">Notifications</a></li>
<li class="side-menu-category-item" ng-if="enforceCapabilities && hasCapability('capabilities-read')" ng-class="{'current-page': isState('trafficPortal.private.capabilities')}"><a href="/#!/capabilities">Capabilities</a></li>
<li class="side-menu-category-item" ng-if="enforceCapabilities && hasCapability('api-endpoints-read')" ng-class="{'current-page': isState('trafficPortal.private.endpoints')}"><a href="/#!/endpoints">API Endpoints</a></li>
</ul>
diff --git a/traffic_portal/app/src/common/modules/notifications/NotificationsController.js b/traffic_portal/app/src/common/modules/notifications/NotificationsController.js
index 6dc3ae8..a66a59c 100644
--- a/traffic_portal/app/src/common/modules/notifications/NotificationsController.js
+++ b/traffic_portal/app/src/common/modules/notifications/NotificationsController.js
@@ -17,7 +17,7 @@
* under the License.
*/
-var NotificationsController = function($scope, $rootScope, $interval, cdnService) {
+var NotificationsController = function($rootScope, $scope, $interval, cdnService) {
let interval;
@@ -40,16 +40,15 @@ var NotificationsController = function($scope, $rootScope, $interval, cdnService
}
};
- $scope.$on("$destroy", function() {
- killInterval();
- });
+ $scope.dismissedNotifications = JSON.parse(localStorage.getItem("dismissed_notification_ids")) || [];
- $rootScope.$on('headerController::notificationCreated', function() {
- getNotifications();
- });
+ $scope.dismissNotification = function(notification) {
+ $scope.dismissedNotifications.push(notification.id);
+ localStorage.setItem("dismissed_notification_ids", JSON.stringify($scope.dismissedNotifications));
+ };
- $rootScope.$on('headerController::notificationDeleted', function() {
- getNotifications();
+ $scope.$on("$destroy", function() {
+ killInterval();
});
let init = function () {
@@ -60,5 +59,5 @@ var NotificationsController = function($scope, $rootScope, $interval, cdnService
};
-NotificationsController.$inject = ['$scope', '$rootScope', '$interval', 'cdnService'];
+NotificationsController.$inject = ['$rootScope', '$scope', '$interval', 'cdnService'];
module.exports = NotificationsController;
diff --git a/traffic_portal/app/src/common/modules/notifications/notifications.tpl.html b/traffic_portal/app/src/common/modules/notifications/notifications.tpl.html
index 87ffaad..fc583e7 100644
--- a/traffic_portal/app/src/common/modules/notifications/notifications.tpl.html
+++ b/traffic_portal/app/src/common/modules/notifications/notifications.tpl.html
@@ -18,7 +18,8 @@ under the License.
-->
<div id="notificationsContainer">
- <div class="alert alert-info" ng-repeat="n in notifications">
+ <div ng-if="!dismissedNotifications.includes(n.id)" class="alert alert-dismissable alert-info" ng-repeat="n in notifications | orderBy: 'cdn'">
+ <button type="button" class="close" data-dismiss="alert" aria-hidden="true" ng-click="dismissNotification(n)">×</button>
<div ng-bind-html="n.cdn + ': ' + n.notification + ' (' + n.user + ')' | linky:'_blank'"></div>
</div>
</div>
diff --git a/traffic_portal/app/src/common/modules/table/cdnNotifications/TableCDNNotificationsController.js b/traffic_portal/app/src/common/modules/table/cdnNotifications/TableCDNNotificationsController.js
new file mode 100644
index 0000000..761e380
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/cdnNotifications/TableCDNNotificationsController.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+var TableCDNNotificationsController = function(cdn, notifications, filter, $controller, $scope) {
+
+ // extends the TableNotificationsController to inherit common methods
+ angular.extend(this, $controller('TableNotificationsController', { tableName: 'cdnNotifications', notifications: notifications, filter: filter, $scope: $scope }));
+
+ $scope.cdn = cdn;
+};
+
+TableCDNNotificationsController.$inject = ['cdn', 'notifications', 'filter', '$controller', '$scope'];
+module.exports = TableCDNNotificationsController;
diff --git a/traffic_portal/app/src/common/modules/table/cdnNotifications/index.js b/traffic_portal/app/src/common/modules/table/cdnNotifications/index.js
new file mode 100644
index 0000000..23d8906
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/cdnNotifications/index.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = angular.module('trafficPortal.table.cdnNotifications', [])
+ .controller('TableCDNNotificationsController', require('./TableCDNNotificationsController'));
diff --git a/traffic_portal/app/src/common/modules/table/cdnNotifications/table.cdnNotifications.tpl.html b/traffic_portal/app/src/common/modules/table/cdnNotifications/table.cdnNotifications.tpl.html
new file mode 100644
index 0000000..2e41759
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/cdnNotifications/table.cdnNotifications.tpl.html
@@ -0,0 +1,76 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<div class="x_panel">
+ <div class="x_title">
+ <ol class="breadcrumb pull-left">
+ <li><a href="#!/cdns">CDNs</a></li>
+ <li><a ng-href="#!/cdns/{{cdn.id}}">{{::cdn.name}}</a></li>
+ <li class="active">Notifications</li>
+ </ol>
+ <div class="pull-right">
+ <div class="form-inline" role="search">
+ <input id="quickSearch" name="quickSearch" type="search" class="form-control text-input" placeholder="Quick search..." ng-model="quickSearch" ng-change="onQuickSearchChanged()" aria-label="Search"/>
+ <div class="input-group text-input">
+ <span class="input-group-addon">
+ <label for="pageSize">Page size</label>
+ </span>
+ <input id="pageSize" name="pageSize" type="number" min="1" class="form-control" placeholder="100" ng-model="pageSize" ng-change="onPageSizeChanged()" aria-label="Page Size"/>
+ </div>
+ <div id="toggleColumns" class="btn-group" role="group" title="Select Table Columns" uib-dropdown is-open="columnSettings.isopen">
+ <button type="button" class="btn btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
+ <i class="fa fa-columns"></i>
+ <span class="caret"></span>
+ </button>
+ <menu ng-click="$event.stopPropagation()" class="column-settings dropdown-menu-right dropdown-menu" uib-dropdown-menu>
+ <li role="menuitem" ng-repeat="c in gridOptions.columnApi.getAllColumns() | orderBy:'colDef.headerName'">
+ <div class="checkbox">
+ <label><input type="checkbox" ng-checked="c.isVisible()" ng-click="toggleVisibility(c.colId)">{{::c.colDef.headerName}}</label>
+ </div>
+ </li>
+ </menu>
+ </div>
+ <div class="btn-group" role="group" uib-dropdown is-open="more.isopen">
+ <button name="moreBtn" type="button" class="btn btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
+ More
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu-right dropdown-menu" uib-dropdown-menu>
+ <li role="menuitem"><button class="menu-item-button" type="button" ng-click="createNotification(cdn)">Create Notification</button></li>
+ <li class="divider"></li>
+ <li role="menuitem"><button class="menu-item-button" type="button" ng-click="clearTableFilters()">Clear Table Filters</button></li>
+ <li role="menuitem"><button class="menu-item-button" type="button" ng-click="exportCSV()">Export CSV</button></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div class="clearfix"></div>
+ </div>
+ <div class="x_content">
+ <div style="height: 740px;" ag-grid="gridOptions" class="ag-theme-alpine"></div>
+ </div>
+</div>
+
+<menu id="context-menu" class="dropdown-menu" ng-style="menuStyle" type="contextmenu" ng-show="showMenu">
+ <ul>
+ <li role="menuitem">
+ <button type="button" ng-click="confirmDeleteNotification(notification, $event)">Delete Notification</button>
+ </li>
+ </ul>
+</menu>
diff --git a/traffic_portal/app/src/common/modules/table/cdns/TableCDNsController.js b/traffic_portal/app/src/common/modules/table/cdns/TableCDNsController.js
index 0127172..dc17547 100644
--- a/traffic_portal/app/src/common/modules/table/cdns/TableCDNsController.js
+++ b/traffic_portal/app/src/common/modules/table/cdns/TableCDNsController.js
@@ -173,6 +173,12 @@ var TableCDNsController = function(cdns, $location, $scope, $state, $uibModal, $
click: function ($itemScope) {
locationUtils.navigateToPath('/cdns/' + $itemScope.cdn.id + '/servers');
}
+ },
+ {
+ text: 'Manage Notifications',
+ click: function ($itemScope) {
+ locationUtils.navigateToPath('/cdns/' + $itemScope.cdn.id + '/notifications');
+ }
}
];
diff --git a/traffic_portal/app/src/common/modules/table/notifications/TableNotificationsController.js b/traffic_portal/app/src/common/modules/table/notifications/TableNotificationsController.js
new file mode 100644
index 0000000..3581a47
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/notifications/TableNotificationsController.js
@@ -0,0 +1,345 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+var TableNotificationsController = function(tableName, notifications, filter, $scope, $state, $uibModal, $document, dateUtils, cdnService) {
+
+ /**
+ * Gets value to display a default tooltip.
+ */
+ function defaultTooltip(params) {
+ return params.value;
+ }
+
+ function dateCellFormatter(params) {
+ return params.value.toUTCString();
+ }
+
+ let columns = [
+ {
+ headerName: "Created (UTC)",
+ field: "lastUpdated",
+ hide: false,
+ filter: "agDateColumnFilter",
+ tooltipValueGetter: dateCellFormatter,
+ valueFormatter: dateCellFormatter
+ },
+ {
+ headerName: "User",
+ field: "user",
+ hide: false
+ },
+ {
+ headerName: "CDN",
+ field: "cdn",
+ hide: false
+ },
+ {
+ headerName: "Notification",
+ field: "notification",
+ hide: false
+ }
+ ];
+
+ /** All of the notifications - lastUpdated fields converted to actual Date */
+ $scope.notifications = notifications.map(
+ function(x) {
+ x.lastUpdated = x.lastUpdated ? new Date(x.lastUpdated.replace("+00", "Z")) : x.lastUpdated;
+ });
+
+ $scope.quickSearch = '';
+
+ $scope.pageSize = 100;
+
+ /** Options, configuration, data and callbacks for the ag-grid table. */
+ $scope.gridOptions = {
+ columnDefs: columns,
+ enableCellTextSelection: true,
+ suppressMenuHide: true,
+ multiSortKey: 'ctrl',
+ alwaysShowVerticalScroll: true,
+ defaultColDef: {
+ filter: true,
+ sortable: true,
+ resizable: true,
+ tooltipValueGetter: defaultTooltip
+ },
+ rowData: notifications,
+ pagination: true,
+ paginationPageSize: $scope.pageSize,
+ rowBuffer: 0,
+ onColumnResized: function(params) {
+ localStorage.setItem(tableName + "_table_columns", JSON.stringify($scope.gridOptions.columnApi.getColumnState()));
+ },
+ tooltipShowDelay: 500,
+ allowContextMenuWithControlKey: true,
+ preventDefaultOnContextMenu: true,
+ onCellContextMenu: function(params) {
+ $scope.showMenu = true;
+ $scope.menuStyle.left = String(params.event.clientX) + "px";
+ $scope.menuStyle.top = String(params.event.clientY) + "px";
+ $scope.menuStyle.bottom = "unset";
+ $scope.menuStyle.right = "unset";
+ $scope.$apply();
+ const boundingRect = document.getElementById("context-menu").getBoundingClientRect();
+
+ if (boundingRect.bottom > window.innerHeight){
+ $scope.menuStyle.bottom = String(window.innerHeight - params.event.clientY) + "px";
+ $scope.menuStyle.top = "unset";
+ }
+ if (boundingRect.right > window.innerWidth) {
+ $scope.menuStyle.right = String(window.innerWidth - params.event.clientX) + "px";
+ $scope.menuStyle.left = "unset";
+ }
+ $scope.notification = params.data;
+ $scope.$apply();
+ },
+ onColumnVisible: function(params) {
+ if (params.visible){
+ return;
+ }
+ for (let column of params.columns) {
+ if (column.filterActive) {
+ const filterModel = $scope.gridOptions.api.getFilterModel();
+ if (column.colId in filterModel) {
+ delete filterModel[column.colId];
+ $scope.gridOptions.api.setFilterModel(filterModel);
+ }
+ }
+ }
+ },
+ onFirstDataRendered: function() {
+ try {
+ const filterState = JSON.parse(localStorage.getItem(tableName + "_table_filters")) || {};
+ $scope.gridOptions.api.setFilterModel(filterState);
+ } catch (e) {
+ console.error("Failure to load stored filter state:", e);
+ }
+
+ $scope.gridOptions.api.addEventListener("filterChanged", function() {
+ localStorage.setItem(tableName + "_table_filters", JSON.stringify($scope.gridOptions.api.getFilterModel()));
+ });
+ },
+ onGridReady: function() {
+ try {
+ // need to create the show/hide column checkboxes and bind to the current visibility
+ const colstates = JSON.parse(localStorage.getItem(tableName + "_table_columns"));
+ if (colstates) {
+ if (!$scope.gridOptions.columnApi.setColumnState(colstates)) {
+ console.error("Failed to load stored column state: one or more columns not found");
+ }
+ } else {
+ $scope.gridOptions.api.sizeColumnsToFit();
+ }
+ } catch (e) {
+ console.error("Failure to retrieve required column info from localStorage (key=" + tableName + "_table_columns):", e);
+ }
+
+ try {
+ const sortState = JSON.parse(localStorage.getItem(tableName + "_table_sort"));
+ $scope.gridOptions.api.setSortModel(sortState);
+ } catch (e) {
+ console.error("Failure to load stored sort state:", e);
+ }
+
+ try {
+ $scope.quickSearch = localStorage.getItem(tableName + "_quick_search");
+ $scope.gridOptions.api.setQuickFilter($scope.quickSearch);
+ } catch (e) {
+ console.error("Failure to load stored quick search:", e);
+ }
+
+ try {
+ const ps = localStorage.getItem(tableName + "_page_size");
+ if (ps && ps > 0) {
+ $scope.pageSize = Number(ps);
+ $scope.gridOptions.api.paginationSetPageSize($scope.pageSize);
+ }
+ } catch (e) {
+ console.error("Failure to load stored page size:", e);
+ }
+
+ try {
+ const page = parseInt(localStorage.getItem(tableName + "_table_page"));
+ const totalPages = $scope.gridOptions.api.paginationGetTotalPages();
+ if (page !== undefined && page > 0 && page <= totalPages-1) {
+ $scope.gridOptions.api.paginationGoToPage(page);
+ }
+ } catch (e) {
+ console.error("Failed to load stored page number:", e);
+ }
+
+ $scope.gridOptions.api.addEventListener("paginationChanged", function() {
+ localStorage.setItem(tableName + "_table_page", $scope.gridOptions.api.paginationGetCurrentPage());
+ });
+
+ $scope.gridOptions.api.addEventListener("sortChanged", function() {
+ localStorage.setItem(tableName + "_table_sort", JSON.stringify($scope.gridOptions.api.getSortModel()));
+ });
+
+ $scope.gridOptions.api.addEventListener("columnMoved", function() {
+ localStorage.setItem(tableName + "_table_columns", JSON.stringify($scope.gridOptions.columnApi.getColumnState()));
+ });
+
+ $scope.gridOptions.api.addEventListener("columnVisible", function() {
+ $scope.gridOptions.api.sizeColumnsToFit();
+ try {
+ const colStates = $scope.gridOptions.columnApi.getColumnState();
+ localStorage.setItem(tableName + "_table_columns", JSON.stringify(colStates));
+ } catch (e) {
+ console.error("Failed to store column defs to local storage:", e);
+ }
+ });
+ },
+ colResizeDefault: "shift"
+ };
+
+ /** This is used to position the context menu under the cursor. */
+ $scope.menuStyle = {
+ left: 0,
+ top: 0,
+ };
+
+ /** Toggles the visibility of a column that has the ID provided as 'col'. */
+ $scope.toggleVisibility = function(col) {
+ const visible = $scope.gridOptions.columnApi.getColumn(col).isVisible();
+ $scope.gridOptions.columnApi.setColumnVisible(col, !visible);
+ };
+
+ /** Downloads the table as a CSV */
+ $scope.exportCSV = function() {
+ const params = {
+ allColumns: true,
+ fileName: "invalidation_requests.csv",
+ };
+ $scope.gridOptions.api.exportDataAsCsv(params);
+ }
+
+ $scope.onQuickSearchChanged = function() {
+ $scope.gridOptions.api.setQuickFilter($scope.quickSearch);
+ localStorage.setItem(tableName + "_quick_search", $scope.quickSearch);
+ };
+
+ $scope.onPageSizeChanged = function() {
+ const value = Number($scope.pageSize);
+ $scope.gridOptions.api.paginationSetPageSize(value);
+ localStorage.setItem(tableName + "_page_size", value);
+ };
+
+ $scope.clearTableFilters = function() {
+ // clear the quick search
+ $scope.quickSearch = '';
+ $scope.onQuickSearchChanged();
+ // clear any column filters
+ $scope.gridOptions.api.setFilterModel(null);
+ };
+
+ $scope.selectCDNandCreateNotification = function() {
+ const params = {
+ title: 'Create Notification',
+ message: "Please select a CDN"
+ };
+ const modalInstance = $uibModal.open({
+ templateUrl: 'common/modules/dialog/select/dialog.select.tpl.html',
+ controller: 'DialogSelectController',
+ size: 'md',
+ resolve: {
+ params: function () {
+ return params;
+ },
+ collection: function(cdnService) {
+ return cdnService.getCDNs();
+ }
+ }
+ });
+ modalInstance.result.then(function(cdn) {
+ $scope.createNotification(cdn);
+ }, function () {
+ // do nothing
+ });
+ };
+
+ $scope.createNotification = function(cdn) {
+ const params = {
+ title: 'Create ' + cdn.name + ' Notification',
+ message: 'What is the content of your notification for the ' + cdn.name + ' CDN?'
+ };
+ const modalInstance = $uibModal.open({
+ templateUrl: 'common/modules/dialog/input/dialog.input.tpl.html',
+ controller: 'DialogInputController',
+ size: 'md',
+ resolve: {
+ params: function () {
+ return params;
+ }
+ }
+ });
+ modalInstance.result.then(function(notification) {
+ cdnService.createNotification(cdn, notification).
+ then(
+ function() {
+ $state.reload();
+ }
+ );
+ }, function () {
+ // do nothing
+ });
+ };
+
+ $scope.confirmDeleteNotification = function(notification, event) {
+ event.stopPropagation();
+ const params = {
+ title: 'Delete Notification',
+ message: 'Are you sure you want to delete the notification for the ' + notification.cdn + ' CDN? This will remove the notification from the view of all users.'
+ };
+ const modalInstance = $uibModal.open({
+ templateUrl: 'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+ controller: 'DialogConfirmController',
+ size: 'md',
+ resolve: {
+ params: function () {
+ return params;
+ }
+ }
+ });
+ modalInstance.result.then(function() {
+ cdnService.deleteNotification({ id: notification.id }).
+ then(
+ function() {
+ $state.reload();
+ }
+ );
+ }, function () {
+ // do nothing
+ });
+ };
+
+ /**** Initialization code, including loading user columns from localstorage ****/
+ angular.element(document).ready(function () {
+ // clicks outside the context menu will hide it
+ $document.bind("click", function(e) {
+ $scope.showMenu = false;
+ e.stopPropagation();
+ $scope.$apply();
+ });
+ });
+
+};
+
+TableNotificationsController.$inject = ['tableName', 'notifications', 'filter', '$scope', '$state', '$uibModal', '$document', 'dateUtils', 'cdnService'];
+module.exports = TableNotificationsController;
diff --git a/traffic_portal/app/src/common/modules/table/notifications/index.js b/traffic_portal/app/src/common/modules/table/notifications/index.js
new file mode 100644
index 0000000..d9291a8
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/notifications/index.js
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = angular.module('trafficPortal.table.notifications', [])
+ .controller('TableNotificationsController', require('./TableNotificationsController'));
diff --git a/traffic_portal/app/src/common/modules/table/notifications/table.notifications.tpl.html b/traffic_portal/app/src/common/modules/table/notifications/table.notifications.tpl.html
new file mode 100644
index 0000000..67cfe47
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/notifications/table.notifications.tpl.html
@@ -0,0 +1,74 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<div class="x_panel">
+ <div class="x_title">
+ <ol class="breadcrumb pull-left">
+ <li class="active">Notifications</li>
+ </ol>
+ <div class="pull-right">
+ <div class="form-inline" role="search">
+ <input id="quickSearch" name="quickSearch" type="search" class="form-control text-input" placeholder="Quick search..." ng-model="quickSearch" ng-change="onQuickSearchChanged()" aria-label="Search"/>
+ <div class="input-group text-input">
+ <span class="input-group-addon">
+ <label for="pageSize">Page size</label>
+ </span>
+ <input id="pageSize" name="pageSize" type="number" min="1" class="form-control" placeholder="100" ng-model="pageSize" ng-change="onPageSizeChanged()" aria-label="Page Size"/>
+ </div>
+ <div id="toggleColumns" class="btn-group" role="group" title="Select Table Columns" uib-dropdown is-open="columnSettings.isopen">
+ <button type="button" class="btn btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
+ <i class="fa fa-columns"></i>
+ <span class="caret"></span>
+ </button>
+ <menu ng-click="$event.stopPropagation()" class="column-settings dropdown-menu-right dropdown-menu" uib-dropdown-menu>
+ <li role="menuitem" ng-repeat="c in gridOptions.columnApi.getAllColumns() | orderBy:'colDef.headerName'">
+ <div class="checkbox">
+ <label><input type="checkbox" ng-checked="c.isVisible()" ng-click="toggleVisibility(c.colId)">{{::c.colDef.headerName}}</label>
+ </div>
+ </li>
+ </menu>
+ </div>
+ <div class="btn-group" role="group" uib-dropdown is-open="more.isopen">
+ <button name="moreBtn" type="button" class="btn btn-default dropdown-toggle" uib-dropdown-toggle aria-haspopup="true" aria-expanded="false">
+ More
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu-right dropdown-menu" uib-dropdown-menu>
+ <li role="menuitem"><button class="menu-item-button" type="button" ng-click="selectCDNandCreateNotification()">Create Notification</button></li>
+ <li class="divider"></li>
+ <li role="menuitem"><button class="menu-item-button" type="button" ng-click="clearTableFilters()">Clear Table Filters</button></li>
+ <li role="menuitem"><button class="menu-item-button" type="button" ng-click="exportCSV()">Export CSV</button></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div class="clearfix"></div>
+ </div>
+ <div class="x_content">
+ <div style="height: 740px;" ag-grid="gridOptions" class="ag-theme-alpine"></div>
+ </div>
+</div>
+
+<menu id="context-menu" class="dropdown-menu" ng-style="menuStyle" type="contextmenu" ng-show="showMenu">
+ <ul>
+ <li role="menuitem">
+ <button type="button" ng-click="confirmDeleteNotification(notification, $event)">Delete Notification</button>
+ </li>
+ </ul>
+</menu>
diff --git a/traffic_portal/app/src/modules/private/cdns/notifications/index.js b/traffic_portal/app/src/modules/private/cdns/notifications/index.js
new file mode 100644
index 0000000..3e04cb2
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/cdns/notifications/index.js
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = angular.module('trafficPortal.private.cdns.notifications', [])
+ .config(function($stateProvider, $urlRouterProvider) {
+ $stateProvider
+ .state('trafficPortal.private.cdns.notifications', {
+ url: '/{cdnId}/notifications',
+ views: {
+ cdnsContent: {
+ templateUrl: 'common/modules/table/cdnNotifications/table.cdnNotifications.tpl.html',
+ controller: 'TableCDNNotificationsController',
+ resolve: {
+ cdn: function($stateParams, cdnService) {
+ return cdnService.getCDN($stateParams.cdnId);
+ },
+ notifications: function(cdn, cdnService) {
+ return cdnService.getNotifications({ cdn: cdn.name, orderby: 'lastUpdated' });
+ },
+ filter: function(cdn) {
+ return {
+ cdn: {
+ filterType: "text",
+ type: "equals",
+ filter: cdn.name
+ }
+ }
+ }
+ }
+ }
+ }
+ })
+ ;
+ $urlRouterProvider.otherwise('/');
+ });
diff --git a/traffic_portal/app/src/modules/private/notifications/index.js b/traffic_portal/app/src/modules/private/notifications/index.js
new file mode 100644
index 0000000..e029b2b
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/notifications/index.js
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = angular.module('trafficPortal.private.notifications', [])
+ .config(function($stateProvider, $urlRouterProvider) {
+ $stateProvider
+ .state('trafficPortal.private.notifications', {
+ url: 'notifications',
+ abstract: true,
+ views: {
+ privateContent: {
+ templateUrl: 'modules/private/notifications/notifications.tpl.html'
+ }
+ }
+ })
+ ;
+ $urlRouterProvider.otherwise('/');
+ });
diff --git a/traffic_portal/app/src/modules/private/notifications/list/index.js b/traffic_portal/app/src/modules/private/notifications/list/index.js
new file mode 100644
index 0000000..9700eda
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/notifications/list/index.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = angular.module('trafficPortal.private.notifications.list', [])
+ .config(function($stateProvider, $urlRouterProvider) {
+ $stateProvider
+ .state('trafficPortal.private.notifications.list', {
+ url: '',
+ views: {
+ notificationsContent: {
+ templateUrl: 'common/modules/table/notifications/table.notifications.tpl.html',
+ controller: 'TableNotificationsController',
+ resolve: {
+ tableName: function() {
+ return 'notifications';
+ },
+ notifications: function(cdnService) {
+ return cdnService.getNotifications();
+ },
+ filter: function() {
+ return null;
+ }
+ }
+ }
+ }
+ })
+ ;
+ $urlRouterProvider.otherwise('/');
+ });
diff --git a/traffic_portal/app/src/common/modules/notifications/notifications.tpl.html b/traffic_portal/app/src/modules/private/notifications/notifications.tpl.html
similarity index 78%
copy from traffic_portal/app/src/common/modules/notifications/notifications.tpl.html
copy to traffic_portal/app/src/modules/private/notifications/notifications.tpl.html
index 87ffaad..e79d4fc 100644
--- a/traffic_portal/app/src/common/modules/notifications/notifications.tpl.html
+++ b/traffic_portal/app/src/modules/private/notifications/notifications.tpl.html
@@ -17,8 +17,6 @@ specific language governing permissions and limitations
under the License.
-->
-<div id="notificationsContainer">
- <div class="alert alert-info" ng-repeat="n in notifications">
- <div ng-bind-html="n.cdn + ': ' + n.notification + ' (' + n.user + ')' | linky:'_blank'"></div>
- </div>
+<div id="cdnNotificationsContainer">
+ <div ui-view="notificationsContent"></div>
</div>