From 7817d4bd0b86e3ae33e2890f214e7534f060a6c2 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sun, 21 Jan 2018 17:26:24 +0100 Subject: [PATCH] extension(storidge): add Storidge extension (#1581) --- app/__module.js | 2 + .../createVolume/createVolumeController.js | 18 +- app/components/createVolume/createvolume.html | 8 + app/components/sidebar/sidebar.html | 12 ++ app/components/sidebar/sidebarController.js | 6 +- app/components/volumes/volumesController.js | 1 + app/extensions/storidge/__module.js | 103 +++++++++ .../storidgeClusterEventsDatatable.html | 89 ++++++++ .../storidgeClusterEventsDatatable.js | 13 ++ .../storidgeNodesDatatable.html | 92 ++++++++ .../nodes-datatable/storidgeNodesDatatable.js | 13 ++ .../storidgeProfileSelector.html | 8 + .../storidgeProfileSelector.js | 7 + .../storidgeProfileSelectorController.js | 17 ++ .../storidgeProfilesDatatable.html | 84 ++++++++ .../storidgeProfilesDatatable.js | 14 ++ app/extensions/storidge/models/events.js | 6 + app/extensions/storidge/models/info.js | 17 ++ app/extensions/storidge/models/node.js | 6 + app/extensions/storidge/models/profile.js | 57 +++++ app/extensions/storidge/rest/cluster.js | 44 ++++ app/extensions/storidge/rest/node.js | 24 +++ app/extensions/storidge/rest/profile.js | 52 +++++ .../storidge/services/chartService.js | 189 ++++++++++++++++ .../storidge/services/clusterService.js | 58 +++++ app/extensions/storidge/services/manager.js | 48 +++++ .../storidge/services/nodeService.js | 30 +++ .../storidge/services/profileService.js | 53 +++++ .../storidge/views/cluster/cluster.html | 145 +++++++++++++ .../views/cluster/clusterController.js | 87 ++++++++ .../storidge/views/monitor/monitor.html | 201 ++++++++++++++++++ .../views/monitor/monitorController.js | 108 ++++++++++ .../views/profiles/create/createProfile.html | 200 +++++++++++++++++ .../create/createProfileController.js | 72 +++++++ .../views/profiles/edit/editProfile.html | 198 +++++++++++++++++ .../profiles/edit/editProfileController.js | 98 +++++++++ .../storidge/views/profiles/profiles.html | 123 +++++++++++ .../views/profiles/profilesController.js | 70 ++++++ app/services/chartService.js | 4 +- app/services/extensionManager.js | 36 ++++ app/services/localStorage.js | 9 + app/services/stateManager.js | 7 +- assets/css/app.css | 42 +++- gruntfile.js | 2 +- package.json | 47 ++-- vendor.yml | 2 + yarn.lock | 6 +- 47 files changed, 2484 insertions(+), 44 deletions(-) create mode 100644 app/extensions/storidge/__module.js create mode 100644 app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.html create mode 100644 app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.js create mode 100644 app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.html create mode 100644 app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.js create mode 100644 app/extensions/storidge/components/profileSelector/storidgeProfileSelector.html create mode 100644 app/extensions/storidge/components/profileSelector/storidgeProfileSelector.js create mode 100644 app/extensions/storidge/components/profileSelector/storidgeProfileSelectorController.js create mode 100644 app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.html create mode 100644 app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.js create mode 100644 app/extensions/storidge/models/events.js create mode 100644 app/extensions/storidge/models/info.js create mode 100644 app/extensions/storidge/models/node.js create mode 100644 app/extensions/storidge/models/profile.js create mode 100644 app/extensions/storidge/rest/cluster.js create mode 100644 app/extensions/storidge/rest/node.js create mode 100644 app/extensions/storidge/rest/profile.js create mode 100644 app/extensions/storidge/services/chartService.js create mode 100644 app/extensions/storidge/services/clusterService.js create mode 100644 app/extensions/storidge/services/manager.js create mode 100644 app/extensions/storidge/services/nodeService.js create mode 100644 app/extensions/storidge/services/profileService.js create mode 100644 app/extensions/storidge/views/cluster/cluster.html create mode 100644 app/extensions/storidge/views/cluster/clusterController.js create mode 100644 app/extensions/storidge/views/monitor/monitor.html create mode 100644 app/extensions/storidge/views/monitor/monitorController.js create mode 100644 app/extensions/storidge/views/profiles/create/createProfile.html create mode 100644 app/extensions/storidge/views/profiles/create/createProfileController.js create mode 100644 app/extensions/storidge/views/profiles/edit/editProfile.html create mode 100644 app/extensions/storidge/views/profiles/edit/editProfileController.js create mode 100644 app/extensions/storidge/views/profiles/profiles.html create mode 100644 app/extensions/storidge/views/profiles/profilesController.js create mode 100644 app/services/extensionManager.js diff --git a/app/__module.js b/app/__module.js index 9e02e45c8..649d4cea1 100644 --- a/app/__module.js +++ b/app/__module.js @@ -5,6 +5,7 @@ angular.module('portainer', [ 'ngCookies', 'ngSanitize', 'ngFileUpload', + 'ngMessages', 'angularUtils.directives.dirPagination', 'LocalStorageModule', 'angular-jwt', @@ -40,6 +41,7 @@ angular.module('portainer', [ 'endpointAccess', 'endpoints', 'events', + 'extension.storidge', 'image', 'images', 'initAdmin', diff --git a/app/components/createVolume/createVolumeController.js b/app/components/createVolume/createVolumeController.js index 491276484..f1e542fe4 100644 --- a/app/components/createVolume/createVolumeController.js +++ b/app/components/createVolume/createVolumeController.js @@ -1,6 +1,6 @@ angular.module('createVolume', []) -.controller('CreateVolumeController', ['$q', '$scope', '$state', 'VolumeService', 'PluginService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator', -function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator) { +.controller('CreateVolumeController', ['$q', '$scope', '$state', 'VolumeService', 'PluginService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator', 'ExtensionManager', +function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator, ExtensionManager) { $scope.formValues = { Driver: 'local', @@ -40,6 +40,12 @@ function ($q, $scope, $state, VolumeService, PluginService, ResourceControlServi var name = $scope.formValues.Name; var driver = $scope.formValues.Driver; var driverOptions = $scope.formValues.DriverOptions; + var storidgeProfile = $scope.formValues.StoridgeProfile; + + if (driver === 'cio:latest' && storidgeProfile) { + driverOptions.push({ name: 'profile', value: storidgeProfile.Name }); + } + var volumeConfiguration = VolumeService.createVolumeConfiguration(name, driver, driverOptions); var accessControlData = $scope.formValues.AccessControlData; var userDetails = Authentication.getUserDetails(); @@ -82,5 +88,11 @@ function ($q, $scope, $state, VolumeService, PluginService, ResourceControlServi } } - initView(); + ExtensionManager.init() + .then(function success(data) { + initView(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to initialize extensions'); + }); }]); diff --git a/app/components/createVolume/createvolume.html b/app/components/createVolume/createvolume.html index 6610f24fe..e2ad9c580 100644 --- a/app/components/createVolume/createvolume.html +++ b/app/components/createVolume/createvolume.html @@ -62,6 +62,14 @@ + +
+
+ Storidge +
+ +
+ diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index 7cfde6625..51ba33df1 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -57,6 +57,18 @@ + + diff --git a/app/components/sidebar/sidebarController.js b/app/components/sidebar/sidebarController.js index e60a2f20b..5b7089450 100644 --- a/app/components/sidebar/sidebarController.js +++ b/app/components/sidebar/sidebarController.js @@ -1,6 +1,6 @@ angular.module('sidebar', []) -.controller('SidebarController', ['$q', '$scope', '$state', 'Settings', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService', -function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointProvider, Notifications, Authentication, UserService) { +.controller('SidebarController', ['$q', '$scope', '$state', 'Settings', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService', 'ExtensionManager', +function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointProvider, Notifications, Authentication, UserService, ExtensionManager) { $scope.uiVersion = StateManager.getState().application.version; $scope.displayExternalContributors = StateManager.getState().application.displayExternalContributors; @@ -12,8 +12,10 @@ function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointP var activeEndpointPublicURL = EndpointProvider.endpointPublicURL(); EndpointProvider.setEndpointID(endpoint.Id); EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); + StateManager.updateEndpointState(true) .then(function success() { + ExtensionManager.reset(); $state.go('dashboard'); }) .catch(function error(err) { diff --git a/app/components/volumes/volumesController.js b/app/components/volumes/volumesController.js index 0e21b48b5..1610800a7 100644 --- a/app/components/volumes/volumesController.js +++ b/app/components/volumes/volumesController.js @@ -40,5 +40,6 @@ function ($q, $scope, $state, VolumeService, Notifications) { Notifications.error('Failure', err, 'Unable to retrieve volumes'); }); } + initView(); }]); diff --git a/app/extensions/storidge/__module.js b/app/extensions/storidge/__module.js new file mode 100644 index 000000000..db7ae432c --- /dev/null +++ b/app/extensions/storidge/__module.js @@ -0,0 +1,103 @@ +angular.module('extension.storidge', []) +.config(['$stateRegistryProvider', function ($stateRegistryProvider) { + 'use strict'; + + var storidge = { + name: 'storidge', + abstract: true, + url: '/storidge', + views: { + 'content@': { + template: '
' + }, + 'sidebar@': { + template: '
' + } + } + }; + + var profiles = { + name: 'storidge.profiles', + url: '/profiles', + views: { + 'content@': { + templateUrl: 'app/extensions/storidge/views/profiles/profiles.html', + controller: 'StoridgeProfilesController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }; + + var profileCreation = { + name: 'storidge.profiles.create', + url: '/create', + params: { + profileName: '' + }, + views: { + 'content@': { + templateUrl: 'app/extensions/storidge/views/profiles/create/createProfile.html', + controller: 'CreateProfileController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }; + + var profileEdition = { + name: 'storidge.profiles.edit', + url: '/edit/:id', + views: { + 'content@': { + templateUrl: 'app/extensions/storidge/views/profiles/edit/editProfile.html', + controller: 'EditProfileController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }; + + var cluster = { + name: 'storidge.cluster', + url: '/cluster', + views: { + 'content@': { + templateUrl: 'app/extensions/storidge/views/cluster/cluster.html', + controller: 'StoridgeClusterController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }; + + var monitor = { + name: 'storidge.monitor', + url: '/events', + views: { + 'content@': { + templateUrl: 'app/extensions/storidge/views/monitor/monitor.html', + controller: 'StoridgeMonitorController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }; + + $stateRegistryProvider.register(storidge); + $stateRegistryProvider.register(profiles); + $stateRegistryProvider.register(profileCreation); + $stateRegistryProvider.register(profileEdition); + $stateRegistryProvider.register(cluster); + $stateRegistryProvider.register(monitor); +}]); diff --git a/app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.html b/app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.html new file mode 100644 index 000000000..0bf7bfa50 --- /dev/null +++ b/app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.html @@ -0,0 +1,89 @@ +
+ + +
+
+ {{ $ctrl.title }} +
+
+ + Search + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + Date + + + + + + Category + + + + + + Module + + + + + + Content + + + +
{{ item.Time }}{{ item.Category }}{{ item.Module }}{{ item.Content }}
Loading...
No events available.
+
+ +
+
+
diff --git a/app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.js b/app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.js new file mode 100644 index 000000000..9cc269f33 --- /dev/null +++ b/app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.js @@ -0,0 +1,13 @@ +angular.module('extension.storidge').component('storidgeClusterEventsDatatable', { + templateUrl: 'app/extensions/storidge/components/cluster-events-datatable/storidgeClusterEventsDatatable.html', + controller: 'GenericDatatableController', + bindings: { + title: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + showTextFilter: '<' + } +}); diff --git a/app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.html b/app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.html new file mode 100644 index 000000000..63a697d85 --- /dev/null +++ b/app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.html @@ -0,0 +1,92 @@ +
+ + +
+
+ {{ $ctrl.title }} +
+
+ + Search + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + IP Address + + + + + + Role + + + + + + Status + + + +
{{ item.Name }}{{ item.IP }}{{ item.Role }} + + {{ item.Status }} +
Loading...
No nodes available.
+
+ +
+
+
diff --git a/app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.js b/app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.js new file mode 100644 index 000000000..15c88c5e5 --- /dev/null +++ b/app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.js @@ -0,0 +1,13 @@ +angular.module('extension.storidge').component('storidgeNodesDatatable', { + templateUrl: 'app/extensions/storidge/components/nodes-datatable/storidgeNodesDatatable.html', + controller: 'GenericDatatableController', + bindings: { + title: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + showTextFilter: '<' + } +}); diff --git a/app/extensions/storidge/components/profileSelector/storidgeProfileSelector.html b/app/extensions/storidge/components/profileSelector/storidgeProfileSelector.html new file mode 100644 index 000000000..6629e114c --- /dev/null +++ b/app/extensions/storidge/components/profileSelector/storidgeProfileSelector.html @@ -0,0 +1,8 @@ +
+ +
+ +
+
diff --git a/app/extensions/storidge/components/profileSelector/storidgeProfileSelector.js b/app/extensions/storidge/components/profileSelector/storidgeProfileSelector.js new file mode 100644 index 000000000..46d38607b --- /dev/null +++ b/app/extensions/storidge/components/profileSelector/storidgeProfileSelector.js @@ -0,0 +1,7 @@ +angular.module('extension.storidge').component('storidgeProfileSelector', { + templateUrl: 'app/extensions/storidge/components/profileSelector/storidgeProfileSelector.html', + controller: 'StoridgeProfileSelectorController', + bindings: { + 'storidgeProfile': '=' + } +}); diff --git a/app/extensions/storidge/components/profileSelector/storidgeProfileSelectorController.js b/app/extensions/storidge/components/profileSelector/storidgeProfileSelectorController.js new file mode 100644 index 000000000..b75b7824a --- /dev/null +++ b/app/extensions/storidge/components/profileSelector/storidgeProfileSelectorController.js @@ -0,0 +1,17 @@ +angular.module('extension.storidge') +.controller('StoridgeProfileSelectorController', ['StoridgeProfileService', 'Notifications', +function (StoridgeProfileService, Notifications) { + var ctrl = this; + + function initComponent() { + StoridgeProfileService.profiles() + .then(function success(data) { + ctrl.profiles = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve Storidge profiles'); + }); + } + + initComponent(); +}]); diff --git a/app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.html b/app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.html new file mode 100644 index 000000000..353d20344 --- /dev/null +++ b/app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.html @@ -0,0 +1,84 @@ +
+ + +
+
+ {{ $ctrl.title }} +
+
+ + Search + +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + +
+ + + + + {{ item.Name }} +
Loading...
No profile available.
+
+ +
+
+
diff --git a/app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.js b/app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.js new file mode 100644 index 000000000..a593e497f --- /dev/null +++ b/app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.js @@ -0,0 +1,14 @@ +angular.module('extension.storidge').component('storidgeProfilesDatatable', { + templateUrl: 'app/extensions/storidge/components/profiles-datatable/storidgeProfilesDatatable.html', + controller: 'GenericDatatableController', + bindings: { + title: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + showTextFilter: '<', + removeAction: '<' + } +}); diff --git a/app/extensions/storidge/models/events.js b/app/extensions/storidge/models/events.js new file mode 100644 index 000000000..3fab0d639 --- /dev/null +++ b/app/extensions/storidge/models/events.js @@ -0,0 +1,6 @@ +function StoridgeEventModel(data) { + this.Time = data.time; + this.Category = data.category; + this.Module = data.module; + this.Content = data.content; +} diff --git a/app/extensions/storidge/models/info.js b/app/extensions/storidge/models/info.js new file mode 100644 index 000000000..2db3299ca --- /dev/null +++ b/app/extensions/storidge/models/info.js @@ -0,0 +1,17 @@ +function StoridgeInfoModel(data) { + this.Domain = data.domain; + this.Nodes = data.nodes; + this.Status = data.status; + this.ProvisionedBandwidth = data.provisionedBandwidth; + this.UsedBandwidth = data.usedBandwidth; + this.FreeBandwidth = data.freeBandwidth; + this.TotalBandwidth = data.totalBandwidth; + this.ProvisionedIOPS = data.provisionedIOPS; + this.UsedIOPS = data.usedIOPS; + this.FreeIOPS = data.freeIOPS; + this.TotalIOPS = data.totalIOPS; + this.ProvisionedCapacity = data.provisionedCapacity; + this.UsedCapacity = data.usedCapacity; + this.FreeCapacity = data.freeCapacity; + this.TotalCapacity = data.totalCapacity; +} diff --git a/app/extensions/storidge/models/node.js b/app/extensions/storidge/models/node.js new file mode 100644 index 000000000..e7e46e054 --- /dev/null +++ b/app/extensions/storidge/models/node.js @@ -0,0 +1,6 @@ +function StoridgeNodeModel(name, data) { + this.Name = name; + this.IP = data.ip; + this.Role = data.role; + this.Status = data.status; +} diff --git a/app/extensions/storidge/models/profile.js b/app/extensions/storidge/models/profile.js new file mode 100644 index 000000000..6effb98cf --- /dev/null +++ b/app/extensions/storidge/models/profile.js @@ -0,0 +1,57 @@ +function StoridgeProfileDefaultModel() { + this.Directory = '/cio/'; + this.Capacity = 20; + this.Redundancy = 2; + this.Provisioning = 'thin'; + this.Type = 'ssd'; + this.MinIOPS = 100; + this.MaxIOPS = 2000; + this.MinBandwidth = 1; + this.MaxBandwidth = 100; +} + +function StoridgeProfileListModel(data) { + this.Name = data; + this.Checked = false; +} + +function StoridgeProfileModel(name, data) { + this.Name = name; + this.Directory = data.directory; + this.Capacity = data.capacity; + this.Provisioning = data.provision; + this.Type = data.type; + this.Redundancy = data.level; + + if (data.iops) { + this.MinIOPS = data.iops.min; + this.MaxIOPS = data.iops.max; + } + + if (data.bandwidth) { + this.MinBandwidth = data.bandwidth.min; + this.MaxBandwidth = data.bandwidth.max; + } +} + +function StoridgeCreateProfileRequest(model) { + this.name = model.Name; + this.capacity = model.Capacity; + this.directory = model.Directory; + this.provision = model.Provisioning; + this.type = model.Type; + this.level = model.Redundancy; + if (model.MinIOPS && model.MaxIOPS) { + this.iops = { + min: model.MinIOPS, + max: model.MaxIOPS + }; + } + + if (model.MinBandwidth && model.MaxBandwidth) { + this.bandwidth = { + min: model.MinBandwidth, + max: model.MaxBandwidth + }; + } +} diff --git a/app/extensions/storidge/rest/cluster.js b/app/extensions/storidge/rest/cluster.js new file mode 100644 index 000000000..98b4fc429 --- /dev/null +++ b/app/extensions/storidge/rest/cluster.js @@ -0,0 +1,44 @@ +angular.module('extension.storidge') +.factory('StoridgeCluster', ['$http', 'StoridgeManager', function StoridgeClusterFactory($http, StoridgeManager) { + 'use strict'; + + var service = {}; + + service.queryEvents = function() { + return $http({ + method: 'GET', + url: StoridgeManager.StoridgeAPIURL() + '/events', + skipAuthorization: true, + timeout: 4500, + ignoreLoadingBar: true + }); + }; + + service.queryVersion = function() { + return $http({ + method: 'GET', + url: StoridgeManager.StoridgeAPIURL() + '/version', + skipAuthorization: true + }); + }; + + service.queryInfo = function() { + return $http({ + method: 'GET', + url: StoridgeManager.StoridgeAPIURL() + '/info', + skipAuthorization: true, + timeout: 4500, + ignoreLoadingBar: true + }); + }; + + service.reboot = function() { + return $http({ + method: 'POST', + url: StoridgeManager.StoridgeAPIURL() + '/cluster/reboot', + skipAuthorization: true + }); + }; + + return service; +}]); diff --git a/app/extensions/storidge/rest/node.js b/app/extensions/storidge/rest/node.js new file mode 100644 index 000000000..f69d66da3 --- /dev/null +++ b/app/extensions/storidge/rest/node.js @@ -0,0 +1,24 @@ +angular.module('extension.storidge') +.factory('StoridgeNodes', ['$http', 'StoridgeManager', function StoridgeNodesFactory($http, StoridgeManager) { + 'use strict'; + + var service = {}; + + service.query = function() { + return $http({ + method: 'GET', + url: StoridgeManager.StoridgeAPIURL() + '/nodes', + skipAuthorization: true + }); + }; + + service.inspect = function(id) { + return $http({ + method: 'GET', + url: StoridgeManager.StoridgeAPIURL() + '/nodes/' + id, + skipAuthorization: true + }); + }; + + return service; +}]); diff --git a/app/extensions/storidge/rest/profile.js b/app/extensions/storidge/rest/profile.js new file mode 100644 index 000000000..f08e10122 --- /dev/null +++ b/app/extensions/storidge/rest/profile.js @@ -0,0 +1,52 @@ +angular.module('extension.storidge') +.factory('StoridgeProfiles', ['$http', 'StoridgeManager', function StoridgeProfilesFactory($http, StoridgeManager) { + 'use strict'; + + var service = {}; + + service.create = function(payload) { + return $http({ + method: 'POST', + url: StoridgeManager.StoridgeAPIURL() + '/profiles', + data: payload, + headers: { 'Content-type': 'application/json' }, + skipAuthorization: true + }); + }; + + service.update = function(id, payload) { + return $http({ + method: 'PUT', + url: StoridgeManager.StoridgeAPIURL() + '/profiles/' + id, + data: payload, + headers: { 'Content-type': 'application/json' }, + skipAuthorization: true + }); + }; + + service.query = function() { + return $http({ + method: 'GET', + url: StoridgeManager.StoridgeAPIURL() + '/profiles', + skipAuthorization: true + }); + }; + + service.inspect = function(id) { + return $http({ + method: 'GET', + url: StoridgeManager.StoridgeAPIURL() + '/profiles/' + id, + skipAuthorization: true + }); + }; + + service.delete = function(id) { + return $http({ + method: 'DELETE', + url: StoridgeManager.StoridgeAPIURL() + '/profiles/' + id, + skipAuthorization: true + }); + }; + + return service; +}]); diff --git a/app/extensions/storidge/services/chartService.js b/app/extensions/storidge/services/chartService.js new file mode 100644 index 000000000..de99a9c97 --- /dev/null +++ b/app/extensions/storidge/services/chartService.js @@ -0,0 +1,189 @@ +angular.module('extension.storidge') +.factory('StoridgeChartService', [function StoridgeChartService() { + 'use strict'; + + // Max. number of items to display on a chart + var CHART_LIMIT = 600; + + var service = {}; + + service.CreateCapacityChart = function(context) { + return new Chart(context, { + type: 'doughnut', + data: { + datasets: [ + { + data: [], + backgroundColor: [ + 'rgba(171, 213, 255, 0.7)', + 'rgba(229, 57, 53, 0.7)' + ] + } + ], + labels: [] + }, + options: { + tooltips: { + callbacks: { + label: function(tooltipItem, data) { + var dataset = data.datasets[tooltipItem.datasetIndex]; + var label = data.labels[tooltipItem.index]; + var value = dataset.data[tooltipItem.index]; + return label + ': ' + filesize(value, {base: 10, round: 1}); + } + } + }, + animation: { + duration: 0 + }, + responsiveAnimationDuration: 0, + responsive: true, + hover: { + animationDuration: 0 + } + } + }); + }; + + service.CreateIOPSChart = function(context) { + return new Chart(context, { + type: 'line', + data: { + labels: [], + datasets: [ + { + label: 'IOPS', + data: [], + fill: true, + backgroundColor: 'rgba(151,187,205,0.4)', + borderColor: 'rgba(151,187,205,0.6)', + pointBackgroundColor: 'rgba(151,187,205,1)', + pointBorderColor: 'rgba(151,187,205,1)', + pointRadius: 2, + borderWidth: 2 + } + ] + }, + options: { + animation: { + duration: 0 + }, + responsiveAnimationDuration: 0, + responsive: true, + tooltips: { + mode: 'index', + intersect: false, + position: 'nearest' + }, + hover: { + animationDuration: 0 + }, + scales: { + yAxes: [ + { + ticks: { + beginAtZero: true + } + } + ] + } + } + }); + }; + + service.CreateBandwidthChart = function(context) { + return new Chart(context, { + type: 'line', + data: { + labels: [], + datasets: [ + { + label: 'Bandwidth', + data: [], + fill: true, + backgroundColor: 'rgba(151,187,205,0.4)', + borderColor: 'rgba(151,187,205,0.6)', + pointBackgroundColor: 'rgba(151,187,205,1)', + pointBorderColor: 'rgba(151,187,205,1)', + pointRadius: 2, + borderWidth: 2 + } + ] + }, + options: { + animation: { + duration: 0 + }, + responsiveAnimationDuration: 0, + responsive: true, + tooltips: { + mode: 'index', + intersect: false, + position: 'nearest', + callbacks: { + label: function(tooltipItem, data) { + var datasetLabel = data.datasets[tooltipItem.datasetIndex].label; + return bytePerSecBasedTooltipLabel(datasetLabel, tooltipItem.yLabel); + } + } + }, + hover: { + animationDuration: 0 + }, + scales: { + yAxes: [ + { + ticks: { + beginAtZero: true, + callback: bytePerSecBasedAxisLabel + } + } + ] + } + } + }); + }; + + service.UpdateChart = function(label, value, chart) { + chart.data.labels.push(label); + chart.data.datasets[0].data.push(value); + + if (chart.data.datasets[0].data.length > CHART_LIMIT) { + chart.data.labels.pop(); + chart.data.datasets[0].data.pop(); + } + + chart.update(0); + }; + + service.UpdatePieChart = function(label, value, chart) { + var idx = chart.data.labels.indexOf(label); + if (idx > -1) { + chart.data.datasets[0].data[idx] = value; + } else { + chart.data.labels.push(label); + chart.data.datasets[0].data.push(value); + } + + chart.update(0); + }; + + function bytePerSecBasedTooltipLabel(label, value) { + var processedValue = 0; + if (value > 5) { + processedValue = filesize(value, {base: 10, round: 1}); + } else { + processedValue = value.toFixed(1) + 'B'; + } + return label + ': ' + processedValue + '/s'; + } + + function bytePerSecBasedAxisLabel(value, index, values) { + if (value > 5) { + return filesize(value, {base: 10, round: 1}); + } + return value.toFixed(1) + 'B/s'; + } + + return service; +}]); diff --git a/app/extensions/storidge/services/clusterService.js b/app/extensions/storidge/services/clusterService.js new file mode 100644 index 000000000..41cc9a34c --- /dev/null +++ b/app/extensions/storidge/services/clusterService.js @@ -0,0 +1,58 @@ +angular.module('extension.storidge') +.factory('StoridgeClusterService', ['$q', 'StoridgeCluster', function StoridgeClusterServiceFactory($q, StoridgeCluster) { + 'use strict'; + var service = {}; + + service.reboot = function() { + return StoridgeCluster.reboot(); + }; + + service.info = function() { + var deferred = $q.defer(); + + StoridgeCluster.queryInfo() + .then(function success(response) { + var info = new StoridgeInfoModel(response.data); + deferred.resolve(info); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve Storidge information', err: err }); + }); + + return deferred.promise; + }; + + service.version = function() { + var deferred = $q.defer(); + + StoridgeCluster.queryVersion() + .then(function success(response) { + var version = response.data.version; + deferred.resolve(version); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve Storidge version', err: err }); + }); + + return deferred.promise; + }; + + service.events = function() { + var deferred = $q.defer(); + + StoridgeCluster.queryEvents() + .then(function success(response) { + var events = response.data.map(function(item) { + return new StoridgeEventModel(item); + }); + deferred.resolve(events); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve Storidge events', err: err }); + }); + + return deferred.promise; + }; + + return service; +}]); diff --git a/app/extensions/storidge/services/manager.js b/app/extensions/storidge/services/manager.js new file mode 100644 index 000000000..e57e7c235 --- /dev/null +++ b/app/extensions/storidge/services/manager.js @@ -0,0 +1,48 @@ +angular.module('extension.storidge') +.factory('StoridgeManager', ['$q', 'LocalStorage', 'SystemService', function StoridgeManagerFactory($q, LocalStorage, SystemService) { + 'use strict'; + var service = { + API: '' + }; + + service.init = function() { + var deferred = $q.defer(); + + var storedAPIURL = LocalStorage.getStoridgeAPIURL(); + if (storedAPIURL) { + service.API = storedAPIURL; + deferred.resolve(); + } else { + SystemService.info() + .then(function success(data) { + var endpointAddress = LocalStorage.getEndpointPublicURL(); + var storidgeAPIURL = ''; + if (endpointAddress) { + storidgeAPIURL = 'http://' + endpointAddress + ':8282'; + } else { + var managerIP = data.Swarm.NodeAddr; + storidgeAPIURL = 'http://' + managerIP + ':8282'; + } + + service.API = storidgeAPIURL; + LocalStorage.storeStoridgeAPIURL(storidgeAPIURL); + deferred.resolve(); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve Storidge API URL', err: err }); + }); + } + + return deferred.promise; + }; + + service.reset = function() { + LocalStorage.clearStoridgeAPIURL(); + }; + + service.StoridgeAPIURL = function() { + return service.API; + }; + + return service; +}]); diff --git a/app/extensions/storidge/services/nodeService.js b/app/extensions/storidge/services/nodeService.js new file mode 100644 index 000000000..ea6640b24 --- /dev/null +++ b/app/extensions/storidge/services/nodeService.js @@ -0,0 +1,30 @@ +angular.module('extension.storidge') +.factory('StoridgeNodeService', ['$q', 'StoridgeNodes', function StoridgeNodeServiceFactory($q, StoridgeNodes) { + 'use strict'; + var service = {}; + + service.nodes = function() { + var deferred = $q.defer(); + + StoridgeNodes.query() + .then(function success(response) { + var nodeData = response.data.nodes; + var nodes = []; + + for (var key in nodeData) { + if (nodeData.hasOwnProperty(key)) { + nodes.push(new StoridgeNodeModel(key, nodeData[key])); + } + } + + deferred.resolve(nodes); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve Storidge profiles', err: err }); + }); + + return deferred.promise; + }; + + return service; +}]); diff --git a/app/extensions/storidge/services/profileService.js b/app/extensions/storidge/services/profileService.js new file mode 100644 index 000000000..e758e1938 --- /dev/null +++ b/app/extensions/storidge/services/profileService.js @@ -0,0 +1,53 @@ +angular.module('extension.storidge') +.factory('StoridgeProfileService', ['$q', 'StoridgeProfiles', function StoridgeProfileServiceFactory($q, StoridgeProfiles) { + 'use strict'; + var service = {}; + + service.create = function(model) { + var payload = new StoridgeCreateProfileRequest(model); + return StoridgeProfiles.create(payload); + }; + + service.update = function(model) { + var payload = new StoridgeCreateProfileRequest(model); + return StoridgeProfiles.update(model.Name, payload); + }; + + service.delete = function(profileName) { + return StoridgeProfiles.delete(profileName); + }; + + service.profile = function(profileName) { + var deferred = $q.defer(); + + StoridgeProfiles.inspect(profileName) + .then(function success(response) { + var profile = new StoridgeProfileModel(profileName, response.data); + deferred.resolve(profile); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve Storidge profile details', err: err }); + }); + + return deferred.promise; + }; + + service.profiles = function() { + var deferred = $q.defer(); + + StoridgeProfiles.query() + .then(function success(response) { + var profiles = response.data.profiles.map(function (item) { + return new StoridgeProfileListModel(item); + }); + deferred.resolve(profiles); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve Storidge profiles', err: err }); + }); + + return deferred.promise; + }; + + return service; +}]); diff --git a/app/extensions/storidge/views/cluster/cluster.html b/app/extensions/storidge/views/cluster/cluster.html new file mode 100644 index 000000000..9dfae86a9 --- /dev/null +++ b/app/extensions/storidge/views/cluster/cluster.html @@ -0,0 +1,145 @@ + + + + + + + + Storidge + + + +
+
+ + + + + + + + + + + + + + + + + + +
Domain{{ clusterInfo.Domain }}
Status {{ clusterInfo.Status }}
Version{{ clusterVersion }}
+
+
+ Actions +
+
+
+ + +
+
+
+
+
+
+
+ +
+
+ +
+
+ + diff --git a/app/extensions/storidge/views/cluster/clusterController.js b/app/extensions/storidge/views/cluster/clusterController.js new file mode 100644 index 000000000..371abc8c4 --- /dev/null +++ b/app/extensions/storidge/views/cluster/clusterController.js @@ -0,0 +1,87 @@ +angular.module('extension.storidge') +.controller('StoridgeClusterController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeClusterService', 'StoridgeNodeService', 'StoridgeManager', 'ModalService', +function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNodeService, StoridgeManager, ModalService) { + + $scope.state = { + shutdownInProgress: false, + rebootInProgress: false + }; + + $scope.rebootCluster = function() { + ModalService.confirm({ + title: 'Are you sure?', + message: 'All the nodes in the cluster will reboot during the process. Do you want to reboot the Storidge cluster?', + buttons: { + confirm: { + label: 'Reboot', + className: 'btn-danger' + } + }, + callback: function onConfirm(confirmed) { + if(!confirmed) { return; } + rebootCluster(); + } + }); + }; + + $scope.shutdownCluster = function() { + ModalService.confirm({ + title: 'Are you sure?', + message: 'All the nodes in the cluster will shutdown. Do you want to shutdown the Storidge cluster?', + buttons: { + confirm: { + label: 'Shutdown', + className: 'btn-danger' + } + }, + callback: function onConfirm(confirmed) { + if(!confirmed) { return; } + shutdownCluster(); + } + }); + }; + + function shutdownCluster() { + Notifications.error('Not implemented', {}, 'Not implemented yet'); + $state.reload(); + } + + function rebootCluster() { + $scope.state.rebootInProgress = true; + StoridgeClusterService.reboot() + .then(function success(data) { + Notifications.success('Cluster successfully rebooted'); + $state.reload(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to reboot cluster'); + }) + .finally(function final() { + $scope.state.rebootInProgress = false; + }); + } + + function initView() { + $q.all({ + info: StoridgeClusterService.info(), + version: StoridgeClusterService.version(), + nodes: StoridgeNodeService.nodes() + }) + .then(function success(data) { + $scope.clusterInfo = data.info; + $scope.clusterVersion = data.version; + $scope.clusterNodes = data.nodes; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve cluster information'); + }); + } + + StoridgeManager.init() + .then(function success() { + initView(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to communicate with Storidge API'); + }); +}]); diff --git a/app/extensions/storidge/views/monitor/monitor.html b/app/extensions/storidge/views/monitor/monitor.html new file mode 100644 index 000000000..b4ce1bfc4 --- /dev/null +++ b/app/extensions/storidge/views/monitor/monitor.html @@ -0,0 +1,201 @@ + + + + + + + + Storidge > Cluster monitoring + + + +
+
+ + + +
+ +
+
+ + + + + + + + + + + + + + + +
Capacity available{{ ((info.FreeCapacity * 100) / info.TotalCapacity).toFixed(1) }}%
Provisioned capacity + {{ info.ProvisionedCapacity | humansize }} + + + +
Total capacity{{ info.TotalCapacity | humansize }}
+
+
+
+
+
+ + + +
+ +
+
+ + + + + + + + + + + + + + + +
IOPS available{{ ((info.FreeIOPS * 100) / info.TotalIOPS).toFixed(1) }}%
Provisioned IOPS + {{ info.ProvisionedIOPS | number }} + + + +
Total IOPS{{ info.TotalIOPS | number }}
+
+
+
+
+
+ + + +
+ +
+
+ + + + + + + + + + + + + + + +
Bandwidth available{{ ((info.FreeBandwidth * 100) / info.TotalBandwidth).toFixed(1) }}%
Provisioned bandwidth + {{ info.ProvisionedBandwidth | humansize }} + + + +
Total bandwidth{{ info.TotalBandwidth | humansize }} /s
+
+
+
+
+
+ +
+
+ +
+
+ + diff --git a/app/extensions/storidge/views/monitor/monitorController.js b/app/extensions/storidge/views/monitor/monitorController.js new file mode 100644 index 000000000..64c60b7f1 --- /dev/null +++ b/app/extensions/storidge/views/monitor/monitorController.js @@ -0,0 +1,108 @@ +angular.module('extension.storidge') +.controller('StoridgeMonitorController', ['$q', '$scope', '$interval', '$document', 'Notifications', 'StoridgeClusterService', 'StoridgeChartService', 'StoridgeManager', 'ModalService', +function ($q, $scope, $interval, $document, Notifications, StoridgeClusterService, StoridgeChartService, StoridgeManager, ModalService) { + + $scope.$on('$destroy', function() { + stopRepeater(); + }); + + function stopRepeater() { + var repeater = $scope.repeater; + if (angular.isDefined(repeater)) { + $interval.cancel(repeater); + repeater = null; + } + } + + function updateIOPSChart(info, chart) { + var usedIOPS = info.UsedIOPS; + var label = moment(new Date()).format('HH:mm:ss'); + + StoridgeChartService.UpdateChart(label, usedIOPS, chart); + } + + function updateBandwithChart(info, chart) { + var usedBandwidth = info.UsedBandwidth; + var label = moment(new Date()).format('HH:mm:ss'); + + StoridgeChartService.UpdateChart(label, usedBandwidth, chart); + } + + function updateCapacityChart(info, chart) { + var usedCapacity = info.UsedCapacity; + var freeCapacity = info.FreeCapacity; + + StoridgeChartService.UpdatePieChart('Free', freeCapacity, chart); + StoridgeChartService.UpdatePieChart('Used', usedCapacity, chart); + } + + function setUpdateRepeater(iopsChart, bandwidthChart, capacityChart) { + var refreshRate = 5000; + $scope.repeater = $interval(function() { + $q.all({ + events: StoridgeClusterService.events(), + info: StoridgeClusterService.info() + }) + .then(function success(data) { + $scope.events = data.events; + var info = data.info; + $scope.info = info; + updateIOPSChart(info, iopsChart); + updateBandwithChart(info, bandwidthChart); + updateCapacityChart(info, capacityChart); + }) + .catch(function error(err) { + stopRepeater(); + Notifications.error('Failure', err, 'Unable to retrieve cluster information'); + }); + }, refreshRate); + } + + function startViewUpdate(iopsChart, bandwidthChart, capacityChart) { + $q.all({ + events: StoridgeClusterService.events(), + info: StoridgeClusterService.info() + }) + .then(function success(data) { + $scope.events = data.events; + var info = data.info; + $scope.info = info; + updateIOPSChart(info, iopsChart); + updateBandwithChart(info, bandwidthChart); + updateCapacityChart(info, capacityChart); + setUpdateRepeater(iopsChart, bandwidthChart, capacityChart); + }) + .catch(function error(err) { + stopRepeater(); + Notifications.error('Failure', err, 'Unable to retrieve cluster information'); + }); + } + + function initCharts() { + var iopsChartCtx = $('#iopsChart'); + var iopsChart = StoridgeChartService.CreateIOPSChart(iopsChartCtx); + + var bandwidthChartCtx = $('#bandwithChart'); + var bandwidthChart = StoridgeChartService.CreateBandwidthChart(bandwidthChartCtx); + + var capacityChartCtx = $('#capacityChart'); + var capacityChart = StoridgeChartService.CreateCapacityChart(capacityChartCtx); + + startViewUpdate(iopsChart, bandwidthChart, capacityChart); + } + + function initView() { + + $document.ready(function() { + initCharts(); + }); + } + + StoridgeManager.init() + .then(function success() { + initView(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to communicate with Storidge API'); + }); +}]); diff --git a/app/extensions/storidge/views/profiles/create/createProfile.html b/app/extensions/storidge/views/profiles/create/createProfile.html new file mode 100644 index 000000000..621a8332c --- /dev/null +++ b/app/extensions/storidge/views/profiles/create/createProfile.html @@ -0,0 +1,200 @@ + + + + Storidge > Profiles > Add profile + + + +
+
+ + +
+ +
+ +
+ +
+
+
+
+
+

This field is required.

+
+
+
+ +
+ Profile configuration +
+ +
+ +
+ +
+
+
+
+
+

This field is required.

+
+
+
+ + +
+ +
+ +
+
+
+
+
+

This field is required.

+

Minimum value for capacity: 1.

+

Maximum value for capacity: 64000.

+
+
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+
+ IOPS +
+
+
+ + +
+
+
+ +
+ +
+ +
+ +
+
+
+
+
+

A value is required for Min IOPS.

+

Minimum value for Min IOPS: 30.

+

Maximum value for Min IOPS: 999999.

+
+
+
+
+
+
+

A value is required for Max IOPS.

+

Minimum value for Max IOPS: 30.

+

Maximum value for Max IOPS: 999999.

+
+
+
+
+ + +
+
+ Bandwidth +
+
+
+ + +
+
+
+ +
+ +
+ +
+ +
+
+
+
+
+

A value is required for Min bandwidth.

+

Minimum value for Min bandwidth: 1.

+

Maximum value for Min bandwidth: 5000.

+
+
+
+
+
+
+

A value is required for Max bandwidth.

+

Minimum value for Max bandwidth: 1.

+

Maximum value for Max bandwidth: 5000.

+
+
+
+
+ +
+ Actions +
+
+
+ +
+
+ +
+
+
+
+
diff --git a/app/extensions/storidge/views/profiles/create/createProfileController.js b/app/extensions/storidge/views/profiles/create/createProfileController.js new file mode 100644 index 000000000..506635697 --- /dev/null +++ b/app/extensions/storidge/views/profiles/create/createProfileController.js @@ -0,0 +1,72 @@ +angular.module('extension.storidge') +.controller('CreateProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', 'StoridgeManager', +function ($scope, $state, $transition$, Notifications, StoridgeProfileService, StoridgeManager) { + + $scope.state = { + NoLimit: true, + LimitIOPS: false, + LimitBandwidth: false, + ManualInputDirectory: false, + actionInProgress: false + }; + + $scope.RedundancyOptions = [ + { value: 2, label: '2-copy' }, + { value: 3, label: '3-copy' } + ]; + + $scope.create = function () { + var profile = $scope.model; + + if (!$scope.state.LimitIOPS) { + delete profile.MinIOPS; + delete profile.MaxIOPS; + } + + if (!$scope.state.LimitBandwidth) { + delete profile.MinBandwidth; + delete profile.MaxBandwidth; + } + + $scope.state.actionInProgress = true; + StoridgeProfileService.create(profile) + .then(function success(data) { + Notifications.success('Profile successfully created'); + $state.go('storidge.profiles'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to create profile'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + }; + + $scope.updatedName = function() { + if (!$scope.state.ManualInputDirectory) { + var profile = $scope.model; + profile.Directory = '/cio/' + profile.Name; + } + }; + + $scope.updatedDirectory = function() { + if (!$scope.state.ManualInputDirectory) { + $scope.state.ManualInputDirectory = true; + } + }; + + function initView() { + var profile = new StoridgeProfileDefaultModel(); + profile.Name = $transition$.params().profileName; + profile.Directory = '/cio/' + profile.Name; + $scope.model = profile; + } + + StoridgeManager.init() + .then(function success() { + initView(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to communicate with Storidge API'); + }); +}]); diff --git a/app/extensions/storidge/views/profiles/edit/editProfile.html b/app/extensions/storidge/views/profiles/edit/editProfile.html new file mode 100644 index 000000000..8aca0bbb6 --- /dev/null +++ b/app/extensions/storidge/views/profiles/edit/editProfile.html @@ -0,0 +1,198 @@ + + + + Storidge > Profiles > {{ profile.Name }} + + + +
+
+ + +
+ +
+ +
+ +
+
+ +
+ Profile configuration +
+ +
+ +
+ +
+
+
+
+
+

This field is required.

+
+
+
+ + +
+ +
+ +
+
+
+
+
+

This field is required.

+

Minimum value for capacity: 1.

+

Maximum value for capacity: 64000.

+
+
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+
+ IOPS +
+
+
+ + +
+
+
+ +
+ +
+ +
+ +
+
+
+
+
+

A value is required for Min IOPS.

+

Minimum value for Min IOPS: 30.

+

Maximum value for Min IOPS: 999999.

+
+
+
+
+
+
+

A value is required for Max IOPS.

+

Minimum value for Max IOPS: 30.

+

Maximum value for Max IOPS: 999999.

+
+
+
+
+ + +
+
+ Bandwidth +
+
+
+ + +
+
+
+ +
+ +
+ +
+ +
+
+
+
+
+

A value is required for Min bandwidth.

+

Minimum value for Min bandwidth: 1.

+

Maximum value for Min bandwidth: 5000.

+
+
+
+
+
+
+

A value is required for Max bandwidth.

+

Minimum value for Max bandwidth: 1.

+

Maximum value for Max bandwidth: 5000.

+
+
+
+
+ + +
+ Actions +
+
+
+ + +
+
+ +
+
+
+
+
diff --git a/app/extensions/storidge/views/profiles/edit/editProfileController.js b/app/extensions/storidge/views/profiles/edit/editProfileController.js new file mode 100644 index 000000000..d36d26738 --- /dev/null +++ b/app/extensions/storidge/views/profiles/edit/editProfileController.js @@ -0,0 +1,98 @@ +angular.module('extension.storidge') +.controller('EditProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', 'StoridgeManager', 'ModalService', +function ($scope, $state, $transition$, Notifications, StoridgeProfileService, StoridgeManager, ModalService) { + + $scope.state = { + NoLimit: false, + LimitIOPS: false, + LimitBandwidth: false, + updateInProgress: false, + deleteInProgress: false + }; + + $scope.RedundancyOptions = [ + { value: 2, label: '2-copy' }, + { value: 3, label: '3-copy' } + ]; + + $scope.update = function() { + + var profile = $scope.profile; + + if (!$scope.state.LimitIOPS) { + delete profile.MinIOPS; + delete profile.MaxIOPS; + } + + if (!$scope.state.LimitBandwidth) { + delete profile.MinBandwidth; + delete profile.MaxBandwidth; + } + + $scope.state.updateInProgress = true; + StoridgeProfileService.update(profile) + .then(function success(data) { + Notifications.success('Profile successfully updated'); + $state.go('storidge.profiles'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to update profile'); + }) + .finally(function final() { + $scope.state.updateInProgress = false; + }); + }; + + $scope.delete = function() { + ModalService.confirmDeletion( + 'Do you want to remove this profile?', + function onConfirm(confirmed) { + if(!confirmed) { return; } + deleteProfile(); + } + ); + }; + + function deleteProfile() { + var profile = $scope.profile; + + $scope.state.deleteInProgress = true; + StoridgeProfileService.delete(profile.Name) + .then(function success(data) { + Notifications.success('Profile successfully deleted'); + $state.go('storidge.profiles'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to delete profile'); + }) + .finally(function final() { + $scope.state.deleteInProgress = false; + }); + } + + function initView() { + StoridgeProfileService.profile($transition$.params().id) + .then(function success(data) { + var profile = data; + if ((profile.MinIOPS && profile.MinIOPS !== 0) || (profile.MaxIOPS && profile.MaxIOPS !== 0)) { + $scope.state.LimitIOPS = true; + } else if ((profile.MinBandwidth && profile.MinBandwidth !== 0) || (profile.MaxBandwidth && profile.MaxBandwidth !== 0)) { + $scope.state.LimitBandwidth = true; + } else { + $scope.state.NoLimit = true; + } + $scope.profile = profile; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve profile details'); + }); + } + + StoridgeManager.init() + .then(function success() { + initView(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to communicate with Storidge API'); + }); +}]); diff --git a/app/extensions/storidge/views/profiles/profiles.html b/app/extensions/storidge/views/profiles/profiles.html new file mode 100644 index 000000000..3eafa095e --- /dev/null +++ b/app/extensions/storidge/views/profiles/profiles.html @@ -0,0 +1,123 @@ + + + + + + + + Storidge > Profiles + + + +
+
+ + + + +
+ +
+ +
+ +
+
+ + +
+
+ Note: The profile will be created using the default properties. +
+
+ +
+
+ + +
+
+
+
+
+
+
+ +
+
+ +
+
+ + + diff --git a/app/extensions/storidge/views/profiles/profilesController.js b/app/extensions/storidge/views/profiles/profilesController.js new file mode 100644 index 000000000..f3bd1c441 --- /dev/null +++ b/app/extensions/storidge/views/profiles/profilesController.js @@ -0,0 +1,70 @@ +angular.module('extension.storidge') +.controller('StoridgeProfilesController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeProfileService', 'StoridgeManager', +function ($q, $scope, $state, Notifications, StoridgeProfileService, StoridgeManager) { + + $scope.state = { + actionInProgress: false + }; + + $scope.formValues = { + Name: '' + }; + + $scope.removeAction = function(selectedItems) { + var actionCount = selectedItems.length; + angular.forEach(selectedItems, function (profile) { + StoridgeProfileService.delete(profile.Name) + .then(function success() { + Notifications.success('Profile successfully removed', profile.Name); + var index = $scope.profiles.indexOf(profile); + $scope.profiles.splice(index, 1); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove profile'); + }) + .finally(function final() { + --actionCount; + if (actionCount === 0) { + $state.reload(); + } + }); + }); + }; + + $scope.create = function() { + var model = new StoridgeProfileDefaultModel(); + model.Name = $scope.formValues.Name; + model.Directory = model.Directory + model.Name; + + $scope.state.actionInProgress = true; + StoridgeProfileService.create(model) + .then(function success(data) { + Notifications.success('Profile successfully created'); + $state.reload(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to create profile'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + }; + + function initView() { + StoridgeProfileService.profiles() + .then(function success(data) { + $scope.profiles = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve profiles'); + }); + } + + StoridgeManager.init() + .then(function success() { + initView(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to communicate with Storidge API'); + }); +}]); diff --git a/app/services/chartService.js b/app/services/chartService.js index 5aa2ac1db..6510f2f5d 100644 --- a/app/services/chartService.js +++ b/app/services/chartService.js @@ -87,8 +87,8 @@ angular.module('portainer.services') label: 'TX on eth0', data: [], fill: false, - backgroundColor: 'rgba(255,180,174,0.5)', - borderColor: 'rgba(255,180,174,0.7)', + backgroundColor: 'rgba(255,180,174,0.4)', + borderColor: 'rgba(255,180,174,0.6)', pointBackgroundColor: 'rgba(255,180,174,1)', pointBorderColor: 'rgba(255,180,174,1)', pointRadius: 2, diff --git a/app/services/extensionManager.js b/app/services/extensionManager.js new file mode 100644 index 000000000..32dfe9f0f --- /dev/null +++ b/app/services/extensionManager.js @@ -0,0 +1,36 @@ +angular.module('portainer.services') +.factory('ExtensionManager', ['$q', 'PluginService', 'StoridgeManager', function ExtensionManagerFactory($q, PluginService, StoridgeManager) { + 'use strict'; + var service = {}; + + service.init = function() { + return $q.all( + StoridgeManager.init() + ); + }; + + service.reset = function() { + StoridgeManager.reset(); + }; + + service.extensions = function() { + var deferred = $q.defer(); + var extensions = []; + + PluginService.volumePlugins() + .then(function success(data) { + var volumePlugins = data; + if (_.includes(volumePlugins, 'cio:latest')) { + extensions.push('storidge'); + } + deferred.resolve(extensions); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve extensions', err: err }); + }); + + return deferred.promise; + }; + + return service; +}]); diff --git a/app/services/localStorage.js b/app/services/localStorage.js index da9f9b670..144ea551e 100644 --- a/app/services/localStorage.js +++ b/app/services/localStorage.js @@ -41,6 +41,15 @@ angular.module('portainer.services') getPaginationLimit: function(key) { return localStorageService.cookie.get('pagination_' + key); }, + storeStoridgeAPIURL: function(url) { + localStorageService.set('STORIDGE_API_URL', url); + }, + getStoridgeAPIURL: function() { + return localStorageService.get('STORIDGE_API_URL'); + }, + clearStoridgeAPIURL: function() { + return localStorageService.remove('STORIDGE_API_URL'); + }, getDataTableOrder: function(key) { return localStorageService.get('datatable_order_' + key); }, diff --git a/app/services/stateManager.js b/app/services/stateManager.js index c9deb5aee..36b3dc3cb 100644 --- a/app/services/stateManager.js +++ b/app/services/stateManager.js @@ -1,5 +1,5 @@ angular.module('portainer.services') -.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', 'APPLICATION_CACHE_VALIDITY', function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService, APPLICATION_CACHE_VALIDITY) { +.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', 'ExtensionManager', 'APPLICATION_CACHE_VALIDITY', function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService, ExtensionManager, APPLICATION_CACHE_VALIDITY) { 'use strict'; var manager = {}; @@ -34,7 +34,6 @@ angular.module('portainer.services') LocalStorage.storeApplicationState(state.application); }; - function assignStateFromStatusAndSettings(status, settings) { state.application.authentication = status.Authentication; state.application.analytics = status.Analytics; @@ -115,13 +114,15 @@ angular.module('portainer.services') } $q.all({ info: SystemService.info(), - version: SystemService.version() + version: SystemService.version(), + extensions: ExtensionManager.extensions() }) .then(function success(data) { var endpointMode = InfoHelper.determineEndpointMode(data.info); var endpointAPIVersion = parseFloat(data.version.ApiVersion); state.endpoint.mode = endpointMode; state.endpoint.apiVersion = endpointAPIVersion; + state.endpoint.extensions = data.extensions; LocalStorage.storeEndpointState(state.endpoint); deferred.resolve(); }) diff --git a/assets/css/app.css b/assets/css/app.css index 341cdcd67..9840c1f67 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -79,6 +79,10 @@ a[ng-click]{ margin-right: 5px; } +.space-left { + margin-left: 5px; +} + .tooltip.portainer-tooltip .tooltip-inner { font-family: Montserrat; background-color: #ffffff; @@ -377,7 +381,27 @@ ul.sidebar .sidebar-list .sidebar-sublist a { text-indent: 35px; font-size: 12px; color: #b2bfdc; - line-height: 40px; + line-height: 36px; +} + +ul.sidebar .sidebar-title { + line-height: 36px; +} +ul.sidebar .sidebar-title .form-control { + height: 36px; + padding: 6px 12px; +} + +ul.sidebar .sidebar-list { + height: 36px; +} + +ul.sidebar .sidebar-list a, ul.sidebar .sidebar-list .sidebar-sublist a { + line-height: 36px; +} + +ul.sidebar .sidebar-list .menu-icon { + line-height: 36px; } ul.sidebar .sidebar-list .sidebar-sublist a.active { @@ -386,27 +410,27 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { background: #2d3e63; } -@media (max-height: 683px) { +@media (max-height: 785px) { ul.sidebar .sidebar-title { - line-height: 28px; + line-height: 26px; } ul.sidebar .sidebar-title .form-control { - height: 28px; - padding: 4px 8px; + height: 26px; + padding: 3px 6px; } ul.sidebar .sidebar-list { - height: 28px; + height: 26px; } ul.sidebar .sidebar-list a, ul.sidebar .sidebar-list .sidebar-sublist a { font-size: 12px; - line-height: 28px; + line-height: 26px; } ul.sidebar .sidebar-list .menu-icon { - line-height: 28px; + line-height: 26px; } } -@media(min-height: 684px) and (max-height: 850px) { +@media(min-height: 786px) and (max-height: 924px) { ul.sidebar .sidebar-title { line-height: 30px; } diff --git a/gruntfile.js b/gruntfile.js index cde30f4da..6974ef870 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -116,7 +116,7 @@ gruntfile_cfg.src = { js: ['app/**/__module.js', 'app/**/*.js', '!app/**/*.spec.js'], jsTpl: ['<%= distdir %>/templates/**/*.js'], html: ['index.html'], - tpl: ['app/components/**/*.html', 'app/directives/**/*.html'], + tpl: ['app/components/**/*.html', 'app/directives/**/*.html', 'app/extensions/**/*.html'], css: ['assets/css/app.css', 'app/**/*.css'] }; diff --git a/package.json b/package.json index 80dabfe94..8211f8a8a 100644 --- a/package.json +++ b/package.json @@ -23,37 +23,38 @@ "node": ">= 0.8.4" }, "dependencies": { + "@uirouter/angularjs": "~1.0.6", "angular": "~1.5.0", "angular-cookies": "~1.5.0", - "angular-ui-bootstrap": "~2.5.0", - "angular-sanitize": "~1.5.0", + "angular-google-analytics": "github:revolunet/angular-google-analytics#~1.1.9", + "angular-json-tree": "1.0.1", + "angular-jwt": "~0.1.8", + "angular-loading-bar": "~0.9.0", + "angular-local-storage": "~0.5.2", + "angular-messages": "~1.5.0", "angular-mocks": "~1.5.0", "angular-resource": "~1.5.0", - "ui-select": "~0.19.6", + "angular-sanitize": "~1.5.0", + "angular-ui-bootstrap": "~2.5.0", "angular-utils-pagination": "~0.11.1", - "angular-local-storage": "~0.5.2", - "angular-jwt": "~0.1.8", - "angular-json-tree": "1.0.1", - "angular-google-analytics": "github:revolunet/angular-google-analytics#~1.1.9", - "bootstrap": "~3.3.6", - "filesize": "~3.3.0", - "jquery": "1.11.1", - "lodash": "4.12.0", - "rdash-ui": "1.0.*", - "moment": "~2.14.1", - "font-awesome": "~4.7.0", - "ng-file-upload": "~12.2.13", - "splitargs": "github:deviantony/splitargs#~0.2.0", - "bootbox": "^4.4.0", - "isteven-angular-multiselect": "~4.0.0", - "toastr": "github:CodeSeven/toastr#~2.1.3", - "xterm": "~2.8.1", - "chart.js": "~2.6.0", "angularjs-slider": "^6.4.0", - "@uirouter/angularjs": "~1.0.6", + "bootbox": "^4.4.0", + "bootstrap": "~3.3.6", + "chart.js": "~2.6.0", "codemirror": "~5.30.0", + "filesize": "~3.3.0", + "font-awesome": "~4.7.0", + "isteven-angular-multiselect": "~4.0.0", + "jquery": "1.11.1", "js-yaml": "~3.10.0", - "angular-loading-bar": "~0.9.0" + "lodash": "4.12.0", + "moment": "~2.14.1", + "ng-file-upload": "~12.2.13", + "rdash-ui": "1.0.*", + "splitargs": "github:deviantony/splitargs#~0.2.0", + "toastr": "github:CodeSeven/toastr#~2.1.3", + "ui-select": "~0.19.6", + "xterm": "~2.8.1" }, "devDependencies": { "autoprefixer": "^7.1.1", diff --git a/vendor.yml b/vendor.yml index de58b9b08..0e0926014 100644 --- a/vendor.yml +++ b/vendor.yml @@ -69,6 +69,7 @@ angular: - node_modules/angular-google-analytics/dist/angular-google-analytics.js - node_modules/angular-jwt/dist/angular-jwt.js - node_modules/angular-local-storage/dist/angular-local-storage.js + - node_modules/angular-messages/angular-messages.js - node_modules/angular-resource/angular-resource.js - node_modules/angular-sanitize/angular-sanitize.js - node_modules/ui-select/dist/select.js @@ -86,6 +87,7 @@ angular: - node_modules/angular-google-analytics/dist/angular-google-analytics.min.js - node_modules/angular-jwt/dist/angular-jwt.min.js - node_modules/angular-local-storage/dist/angular-local-storage.min.js + - node_modules/angular-messages/angular-messages.min.js - node_modules/angular-resource/angular-resource.min.js - node_modules/angular-sanitize/angular-sanitize.min.js - node_modules/ui-select/dist/select.min.js diff --git a/yarn.lock b/yarn.lock index 28c81b08c..6aa370d3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -113,6 +113,10 @@ angular-local-storage@~0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/angular-local-storage/-/angular-local-storage-0.5.2.tgz#7079beb0aa5ca91386d223125efefd13ca0ecd0c" +angular-messages@~1.5.0: + version "1.5.11" + resolved "https://registry.yarnpkg.com/angular-messages/-/angular-messages-1.5.11.tgz#ea99f0163594fcb0a2db701b3038339250decc90" + angular-mocks@~1.5.0: version "1.5.11" resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.5.11.tgz#a0e1dd0ea55fd77ee7a757d75536c5e964c86f81" @@ -2363,7 +2367,7 @@ istanbul@~0.1.40: wordwrap "0.0.x" isteven-angular-multiselect@~4.0.0: - version v4.0.0 + version "4.0.0" resolved "https://registry.yarnpkg.com/isteven-angular-multiselect/-/isteven-angular-multiselect-4.0.0.tgz#70276da5ff3bc4d9a0887dc585ee26a1a26a8ed6" jquery@1.11.1: