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 @@
+
+
+
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 @@
+
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 @@
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 }} |
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
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 }}
+
+
+
+
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
+
+
+
+
+
+
+
+
+
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: