extension(storidge): add Storidge extension (#1581)

pull/1588/head
Anthony Lapenna 2018-01-21 17:26:24 +01:00 committed by GitHub
parent edadce359c
commit 7817d4bd0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 2484 additions and 44 deletions

View File

@ -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',

View File

@ -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');
});
}]);

View File

@ -62,6 +62,14 @@
<!-- !driver-options-input-list -->
</div>
<!-- !driver-options -->
<!-- storidge -->
<div ng-if="formValues.Driver === 'cio:latest'">
<div class="col-sm-12 form-section-title">
Storidge
</div>
<storidge-profile-selector storidge-profile="formValues.StoridgeProfile"></storidge-profile-selector>
</div>
<!-- storidge -->
<!-- access-control -->
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
<!-- !access-control -->

View File

@ -57,6 +57,18 @@
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'">
<a ui-sref="engine" ui-sref-active="active">Engine <span class="menu-icon fa fa-th fa-fw"></span></a>
</li>
<li class="sidebar-title" ng-if="applicationState.endpoint.extensions.length > 0 && isAdmin && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<span>Extensions</span>
</li>
<li class="sidebar-list" ng-if="applicationState.endpoint.extensions.indexOf('storidge') !== -1 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="storidge.cluster" ui-sref-active="active">Storidge <span class="menu-icon fa fa-bolt fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'storidge.cluster' || $state.current.name === 'storidge.profiles' || $state.current.name === 'storidge.monitor' || $state.current.name === 'storidge.profiles.create' || $state.current.name === 'storidge.profiles.edit')">
<a ui-sref="storidge.monitor" ui-sref-active="active">Monitor</a>
</div>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'storidge.cluster' || $state.current.name === 'storidge.profiles' || $state.current.name === 'storidge.monitor' || $state.current.name === 'storidge.profiles.create' || $state.current.name === 'storidge.profiles.edit')">
<a ui-sref="storidge.profiles" ui-sref-active="active">Profiles</a>
</div>
</li>
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
<span>Portainer settings</span>
</li>

View File

@ -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) {

View File

@ -40,5 +40,6 @@ function ($q, $scope, $state, VolumeService, Notifications) {
Notifications.error('Failure', err, 'Unable to retrieve volumes');
});
}
initView();
}]);

View File

@ -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: '<div ui-view="content@"></div>'
},
'sidebar@': {
template: '<div ui-view="sidebar@"></div>'
}
}
};
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);
}]);

View File

@ -0,0 +1,89 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Time')">
Date
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Time' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Time' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Category')">
Category
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Category' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Category' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Module')">
Module
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Module' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Module' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Content')">
Content
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Content' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Content' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>{{ item.Time }}</td>
<td>{{ item.Category }}</td>
<td>{{ item.Module }}</td>
<td>{{ item.Content }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="4" class="text-center text-muted">No events available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@ -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: '<'
}
});

View File

@ -0,0 +1,92 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('IP')">
IP Address
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Role')">
Role
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Role' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Role' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Status')">
Status
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>{{ item.Name }}</td>
<td>{{ item.IP }}</td>
<td>{{ item.Role }}</td>
<td>
<i class="fa fa-heartbeat space-right green-icon"></i>
{{ item.Status }}
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="4" class="text-center text-muted">No nodes available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@ -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: '<'
}
});

View File

@ -0,0 +1,8 @@
<div class="form-group">
<label for="storidge_profile" class="col-sm-2 col-lg-1 control-label text-left">Profile</label>
<div class="col-sm-10 col-lg-11">
<select id="storidge_profile" ng-model="$ctrl.storidgeProfile" ng-options="profile.Name for profile in $ctrl.profiles" class="form-control">
<option selected disabled hidden value="">Select a profile</option>
</select>
</div>
</div>

View File

@ -0,0 +1,7 @@
angular.module('extension.storidge').component('storidgeProfileSelector', {
templateUrl: 'app/extensions/storidge/components/profileSelector/storidgeProfileSelector.html',
controller: 'StoridgeProfileSelectorController',
bindings: {
'storidgeProfile': '='
}
});

View File

@ -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();
}]);

View File

@ -0,0 +1,84 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
</div>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
<i class="fa fa-search" aria-hidden="true"></i> Search
</span>
</div>
</div>
<div class="actionBar">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove
</button>
</div>
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search...">
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-asc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-desc" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="storidge.profiles.edit({id: item.Name})">{{ item.Name }}</a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td class="text-center text-muted">No profile available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
{{ $ctrl.state.selectedItemCount }} item(s) selected
</div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@ -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: '<'
}
});

View File

@ -0,0 +1,6 @@
function StoridgeEventModel(data) {
this.Time = data.time;
this.Category = data.category;
this.Module = data.module;
this.Content = data.content;
}

View File

@ -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;
}

View File

@ -0,0 +1,6 @@
function StoridgeNodeModel(name, data) {
this.Name = name;
this.IP = data.ip;
this.Role = data.role;
this.Status = data.status;
}

View File

@ -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
};
}
}

View File

@ -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;
}]);

View File

@ -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;
}]);

View File

@ -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;
}]);

View File

@ -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;
}]);

View File

@ -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;
}]);

View File

@ -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;
}]);

View File

@ -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;
}]);

View File

@ -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;
}]);

View File

@ -0,0 +1,145 @@
<rd-header>
<rd-header-title title="Storidge cluster">
<a data-toggle="tooltip" title="Refresh" ui-sref="storidge.cluster" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="storidge.cluster">Storidge</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-bolt" title="Cluster details"></rd-widget-header>
<rd-widget-body>
<table class="table">
<tbody>
<tr>
<td>Domain</td>
<td>{{ clusterInfo.Domain }}</td>
</tr>
<tr>
<td>Status</td>
<td><i class="fa fa-heartbeat space-right green-icon"></i> {{ clusterInfo.Status }}</td>
</tr>
<tr>
<td>Version</td>
<td>{{ clusterVersion }}</td>
</tr>
</tbody>
</table>
<form class="form-horizontal">
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-danger btn-sm" ng-click="shutdownCluster()" ng-disabled="state.shutdownInProgress" button-spinner="state.shutdownInProgress">
<span ng-hide="state.updateInProgress"><i class="fa fa-power-off space-right" aria-hidden="true"></i> Shutdown the cluster</span>
<span ng-show="state.updateInProgress">Shutting down cluster...</span>
</button>
<button type="button" class="btn btn-danger btn-sm" ng-click="rebootCluster()" ng-disabled="state.rebootInProgress" button-spinner="state.shutdownInProgress">
<span ng-hide="state.deleteInProgress"><i class="fa fa-refresh space-right" aria-hidden="true"></i> Reboot the cluster</span>
<span ng-show="state.deleteInProgress">Rebooting cluster...</span>
</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<storidge-nodes-datatable
title="Storage nodes" title-icon="fa-object-group"
dataset="clusterNodes" table-key="storidge_nodes"
order-by="Name" show-text-filter="true"
></storidge-nodes-datatable>
</div>
</div>
<!-- <div class="row" ng-if="clusterInfo">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Storage nodes">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ng-click="order('Name')">
Name
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('IP')">
IP address
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('Role')">
Role
<span ng-show="sortType == 'Role' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Role' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('Status')">
Status
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="node in (clusterNodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
<td>{{ node.Name }}</td>
<td>{{ node.IP }}</td>
<td>{{ node.Role }}</td>
<td>
<i class="fa fa-heartbeat space-right green-icon"></i>
{{ node.Status }}
</td>
</tr>
<tr ng-if="!clusterNodes">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="clusterNodes.length === 0">
<td colspan="4" class="text-center text-muted">No nodes available.</td>
</tr>
</tbody>
</table>
<div ng-if="clusterNodes" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div> -->

View File

@ -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');
});
}]);

View File

@ -0,0 +1,201 @@
<rd-header>
<rd-header-title title="Storidge monitor">
<a data-toggle="tooltip" title="Refresh" ui-sref="storidge.monitor" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="storidge.cluster">Storidge</a> &gt; <a ui-sref="storidge.monitor">Cluster monitoring</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-md-4 col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Cluster capacity"></rd-widget-header>
<rd-widget-body>
<div class="chart-container" style="position: relative;">
<canvas id="capacityChart" width="770" height="400"></canvas>
</div>
<div style="margin-top: 10px;" ng-if="info">
<table class="table">
<tbody>
<tr>
<td>Capacity available</td>
<td>{{ ((info.FreeCapacity * 100) / info.TotalCapacity).toFixed(1) }}%</td>
</tr>
<tr>
<td>Provisioned capacity</td>
<td>
{{ info.ProvisionedCapacity | humansize }}
<span ng-if="+(info.ProvisionedCapacity) >= +(info.TotalCapacity)">
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-left: 2px;"></i>
</span>
</td>
</tr>
<tr>
<td>Total capacity</td>
<td>{{ info.TotalCapacity | humansize }}</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-md-4 col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="IOPS usage"></rd-widget-header>
<rd-widget-body>
<div class="chart-container" style="position: relative;">
<canvas id="iopsChart" width="770" height="400"></canvas>
</div>
<div style="margin-top: 10px;" ng-if="info">
<table class="table">
<tbody>
<tr>
<td>IOPS available</td>
<td>{{ ((info.FreeIOPS * 100) / info.TotalIOPS).toFixed(1) }}%</td>
</tr>
<tr>
<td>Provisioned IOPS</td>
<td>
{{ info.ProvisionedIOPS | number }}
<span ng-if="+(info.ProvisionedIOPS) >= +(info.TotalIOPS)">
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-left: 2px;"></i>
</span>
</td>
</tr>
<tr>
<td>Total IOPS</td>
<td>{{ info.TotalIOPS | number }}</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-md-4 col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="Bandwith usage"></rd-widget-header>
<rd-widget-body>
<div class="chart-container" style="position: relative;">
<canvas id="bandwithChart" width="770" height="400"></canvas>
</div>
<div style="margin-top: 10px;" ng-if="info">
<table class="table">
<tbody>
<tr>
<td>Bandwidth available</td>
<td>{{ ((info.FreeBandwidth * 100) / info.TotalBandwidth).toFixed(1) }}%</td>
</tr>
<tr>
<td>Provisioned bandwidth</td>
<td>
{{ info.ProvisionedBandwidth | humansize }}
<span ng-if="+(info.ProvisionedBandwidth) >= +(info.TotalBandwidth)">
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-left: 2px;"></i>
</span>
</td>
</tr>
<tr>
<td>Total bandwidth</td>
<td>{{ info.TotalBandwidth | humansize }} /s</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<storidge-cluster-events-datatable
title="Cluster events" title-icon="fa-history"
dataset="events" table-key="storidge_cluster_events"
order-by="Time" show-text-filter="true" reverse-order="true"
></storidge-cluster-events-datatable>
</div>
</div>
<!-- <div class="row">
<div class="col-md-12">
<rd-widget>
<rd-widget-header icon="fa-history" title="Cluster events">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<a ng-click="order('Time')">
Date
<span ng-show="sortType == 'Time' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Time' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('Category')">
Category
<span ng-show="sortType == 'Category' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Category' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('Module')">
Module
<span ng-show="sortType == 'Module' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Module' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ng-click="order('Content')">
Content
<span ng-show="sortType == 'Content' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Content' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="event in (events | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
<td>{{ event.Time }}</td>
<td>{{ event.Category }}</td>
<td>{{ event.Module }}</td>
<td>{{ event.Content }}</td>
</tr>
<tr ng-if="!events">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="events.length === 0">
<td colspan="4" class="text-center text-muted">No events available.</td>
</tr>
</tbody>
</table>
<div ng-if="events" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div> -->

View File

@ -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');
});
}]);

View File

@ -0,0 +1,200 @@
<rd-header>
<rd-header-title title="Create profile"></rd-header-title>
<rd-header-content>
<a ui-sref="storidge.cluster">Storidge</a> &gt; <a ui-sref="storidge.profiles">Profiles</a> &gt; Add profile
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-md-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="storidgeCreateProfileForm">
<!-- name-input -->
<div class="form-group" ng-class="{ 'has-error': storidgeCreateProfileForm.profile_name.$invalid }">
<label for="profile_name" class="col-sm-2 col-lg-1 control-label text-left">Name</label>
<div class="col-sm-10 col-lg-11">
<input type="text" class="form-control" ng-model="model.Name" name="profile_name" placeholder="e.g. myProfile" ng-change="updatedName()" required>
</div>
</div>
<div class="form-group" ng-show="storidgeCreateProfileForm.profile_name.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeCreateProfileForm.profile_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !name-input -->
<div class="col-sm-12 form-section-title">
Profile configuration
</div>
<!-- directory -->
<div class="form-group" ng-class="{ 'has-error': storidgeCreateProfileForm.profile_directory.$invalid }">
<label for="profile_directory" class="col-sm-2 col-lg-1 control-label text-left">Directory</label>
<div class="col-sm-10 col-lg-11">
<input type="text" class="form-control" ng-model="model.Directory" name="profile_directory" placeholder="e.g. /cio/myProfile" ng-change="updatedDirectory()" required>
</div>
</div>
<div class="form-group" ng-show="storidgeCreateProfileForm.profile_directory.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeCreateProfileForm.profile_directory.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !directory -->
<!-- capacity -->
<div class="form-group" ng-class="{ 'has-error': storidgeCreateProfileForm.profile_capacity.$invalid }">
<label for="profile_capacity" class="col-sm-2 col-lg-1 control-label text-left">Capacity</label>
<div class="col-sm-10 col-lg-11">
<input type="number" class="form-control" ng-model="model.Capacity" name="profile_capacity" ng-min="1" ng-max="64000" placeholder="2" required>
</div>
</div>
<div class="form-group" ng-show="storidgeCreateProfileForm.profile_capacity.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeCreateProfileForm.profile_capacity.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for capacity: 1.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for capacity: 64000.</p>
</div>
</div>
</div>
<!-- !capacity -->
<!-- redundancy -->
<div class="form-group">
<label for="profile_redundancy" class="col-sm-2 col-lg-1 control-label text-left">Redundancy</label>
<div class="col-sm-10 col-lg-11">
<select name="profile_redundancy" ng-model="model.Redundancy" ng-options="+(opt.value) as opt.label for opt in RedundancyOptions" class="form-control">
</select>
</div>
</div>
<!-- !redudancy -->
<!-- provisioning -->
<div class="form-group">
<label for="profile_provisioning" class="col-sm-2 col-lg-1 control-label text-left">Provisioning</label>
<div class="col-sm-10 col-lg-11">
<select name="profile_provisioning" ng-model="model.Provisioning" class="form-control">
<option value="thin">Thin</option>
<option value="thick">Thick</option>
</select>
</div>
</div>
<!-- !provisioning -->
<!-- type -->
<div class="form-group">
<label for="profile_type" class="col-sm-2 col-lg-1 control-label text-left">Type</label>
<div class="col-sm-10 col-lg-11">
<select name="profile_type" ng-model="model.Type" class="form-control">
<option value="ssd">SSD</option>
<option value="hdd">HDD</option>
</select>
</div>
</div>
<!-- !type -->
<!-- iops -->
<div ng-if="!state.LimitBandwidth || state.NoLimit">
<div class="col-sm-12 form-section-title">
IOPS
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="permissions" class="control-label text-left">
Limit IOPS
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="state.LimitIOPS" ng-change="state.NoLimit = (!state.LimitBandwidth && !state.LimitIOPS)"><i></i>
</label>
</div>
</div>
<div class="form-group" ng-if="state.LimitIOPS">
<label for="min_iops" class="col-sm-1 control-label text-left">Min</label>
<div class="col-sm-5" ng-class="{ 'has-error': storidgeCreateProfileForm.min_iops.$invalid }">
<input type="number" class="form-control" ng-model="model.MinIOPS" name="min_iops" ng-min="30" ng-max="999999" placeholder="100" required>
</div>
<label for="max_iops" class="col-sm-1 control-label text-left">Max</label>
<div class="col-sm-5" ng-class="{ 'has-error': storidgeCreateProfileForm.max_iops.$invalid }">
<input type="number" class="form-control" ng-model="model.MaxIOPS" name="max_iops" ng-min="30" ng-max="999999" placeholder="2000" required>
</div>
</div>
<div class="form-group" ng-show="storidgeCreateProfileForm.min_iops.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeCreateProfileForm.min_iops.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Min IOPS.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Min IOPS: 30.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Min IOPS: 999999.</p>
</div>
</div>
</div>
<div class="form-group" ng-show="storidgeCreateProfileForm.max_iops.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeCreateProfileForm.max_iops.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Max IOPS.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Max IOPS: 30.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Max IOPS: 999999.</p>
</div>
</div>
</div>
</div>
<!-- !iops -->
<!-- bandwidth -->
<div ng-if="!state.LimitIOPS || state.NoLimit">
<div class="col-sm-12 form-section-title">
Bandwidth
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="permissions" class="control-label text-left">
Limit bandwidth
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="state.LimitBandwidth" ng-change="state.NoLimit = (!state.LimitBandwidth && !state.LimitIOPS)"><i></i>
</label>
</div>
</div>
<div class="form-group" ng-if="state.LimitBandwidth">
<label for="min_bandwidth" class="col-sm-1 control-label text-left">Min</label>
<div class="col-sm-5" ng-class="{ 'has-error': storidgeCreateProfileForm.min_bandwidth.$invalid }">
<input type="number" class="form-control" ng-model="model.MinBandwidth" name="min_bandwidth" ng-min="1" ng-max="5000" placeholder="1" required>
</div>
<label for="max_bandwidth" class="col-sm-1 control-label text-left">Max</label>
<div class="col-sm-5" ng-class="{ 'has-error': storidgeCreateProfileForm.max_bandwidth.$invalid }">
<input type="number" class="form-control" ng-model="model.MaxBandwidth" name="max_bandwidth" ng-min="1" ng-max="5000" placeholder="100" required>
</div>
</div>
<div class="form-group" ng-show="storidgeCreateProfileForm.min_bandwidth.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeCreateProfileForm.min_bandwidth.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Min bandwidth.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Min bandwidth: 1.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Min bandwidth: 5000.</p>
</div>
</div>
</div>
<div class="form-group" ng-show="storidgeCreateProfileForm.max_bandwidth.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeCreateProfileForm.max_bandwidth.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Max bandwidth.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Max bandwidth: 1.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Max bandwidth: 5000.</p>
</div>
</div>
</div>
</div>
<!-- !bandwidth -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-click="create()" ng-disabled="state.actionInProgress || !storidgeCreateProfileForm.$valid" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Create the profile</span>
<span ng-show="state.actionInProgress">Creating profile...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -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');
});
}]);

View File

@ -0,0 +1,198 @@
<rd-header>
<rd-header-title title="Profile details"></rd-header-title>
<rd-header-content>
<a ui-sref="storidge.cluster">Storidge</a> &gt; <a ui-sref="storidge.profiles">Profiles</a> &gt; {{ profile.Name }}
</rd-header-content>
</rd-header>
<div class="row" ng-if="profile">
<div class="col-md-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="storidgeUpdateProfileForm">
<!-- name-input -->
<div class="form-group">
<label for="profile_name" class="col-sm-2 col-lg-1 control-label text-left">Name</label>
<div class="col-sm-10 col-lg-11">
<input type="text" class="form-control" ng-model="profile.Name" name="profile_name" disabled>
</div>
</div>
<!-- !name-input -->
<div class="col-sm-12 form-section-title">
Profile configuration
</div>
<!-- directory -->
<div class="form-group" ng-class="{ 'has-error': storidgeUpdateProfileForm.profile_directory.$invalid }">
<label for="profile_directory" class="col-sm-2 col-lg-1 control-label text-left">Directory</label>
<div class="col-sm-10 col-lg-11">
<input type="text" class="form-control" ng-model="profile.Directory" name="profile_directory" placeholder="e.g. /cio/myProfile" required>
</div>
</div>
<div class="form-group" ng-show="storidgeUpdateProfileForm.profile_directory.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeUpdateProfileForm.profile_directory.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !directory -->
<!-- capacity -->
<div class="form-group" ng-class="{ 'has-error': storidgeUpdateProfileForm.profile_capacity.$invalid }">
<label for="profile_capacity" class="col-sm-2 col-lg-1 control-label text-left">Capacity</label>
<div class="col-sm-10 col-lg-11">
<input type="number" class="form-control" ng-model="profile.Capacity" name="profile_capacity" ng-min="1" ng-max="64000" placeholder="2" required>
</div>
</div>
<div class="form-group" ng-show="storidgeUpdateProfileForm.profile_capacity.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeUpdateProfileForm.profile_capacity.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for capacity: 1.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for capacity: 64000.</p>
</div>
</div>
</div>
<!-- !capacity -->
<!-- redundancy -->
<div class="form-group">
<label for="profile_redundancy" class="col-sm-2 col-lg-1 control-label text-left">Redundancy</label>
<div class="col-sm-10 col-lg-11">
<select name="profile_redundancy" ng-model="profile.Redundancy" ng-options="+(opt.value) as opt.label for opt in RedundancyOptions" class="form-control">
</select>
</div>
</div>
<!-- !redudancy -->
<!-- provisioning -->
<div class="form-group">
<label for="profile_provisioning" class="col-sm-2 col-lg-1 control-label text-left">Provisioning</label>
<div class="col-sm-10 col-lg-11">
<select name="profile_provisioning" ng-model="profile.Provisioning" class="form-control">
<option value="thin">Thin</option>
<option value="thick">Thick</option>
</select>
</div>
</div>
<!-- !provisioning -->
<!-- type -->
<div class="form-group">
<label for="profile_type" class="col-sm-2 col-lg-1 control-label text-left">Type</label>
<div class="col-sm-10 col-lg-11">
<select name="profile_type" ng-model="profile.Type" class="form-control">
<option value="ssd">SSD</option>
<option value="hdd">HDD</option>
</select>
</div>
</div>
<!-- !type -->
<!-- iops -->
<div ng-if="!state.LimitBandwidth || state.NoLimit">
<div class="col-sm-12 form-section-title">
IOPS
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="permissions" class="control-label text-left">
Limit IOPS
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="state.LimitIOPS" ng-change="state.NoLimit = (!state.LimitBandwidth && !state.LimitIOPS)"><i></i>
</label>
</div>
</div>
<div class="form-group" ng-if="state.LimitIOPS">
<label for="min_iops" class="col-sm-1 control-label text-left">Min</label>
<div class="col-sm-5" ng-class="{ 'has-error': storidgeUpdateProfileForm.min_iops.$invalid }">
<input type="number" class="form-control" ng-model="profile.MinIOPS" name="min_iops" ng-min="30" ng-max="999999" placeholder="100" required>
</div>
<label for="max_iops" class="col-sm-1 control-label text-left">Max</label>
<div class="col-sm-5" ng-class="{ 'has-error': storidgeUpdateProfileForm.max_iops.$invalid }">
<input type="number" class="form-control" ng-model="profile.MaxIOPS" name="max_iops" ng-min="30" ng-max="999999" placeholder="2000" required>
</div>
</div>
<div class="form-group" ng-show="storidgeUpdateProfileForm.min_iops.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeUpdateProfileForm.min_iops.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Min IOPS.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Min IOPS: 30.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Min IOPS: 999999.</p>
</div>
</div>
</div>
<div class="form-group" ng-show="storidgeUpdateProfileForm.max_iops.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeUpdateProfileForm.max_iops.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Max IOPS.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Max IOPS: 30.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Max IOPS: 999999.</p>
</div>
</div>
</div>
</div>
<!-- !iops -->
<!-- bandwidth -->
<div ng-if="!state.LimitIOPS || state.NoLimit">
<div class="col-sm-12 form-section-title">
Bandwidth
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="permissions" class="control-label text-left">
Limit bandwidth
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="state.LimitBandwidth" ng-change="state.NoLimit = (!state.LimitBandwidth && !state.LimitIOPS)"><i></i>
</label>
</div>
</div>
<div class="form-group" ng-if="state.LimitBandwidth">
<label for="min_bandwidth" class="col-sm-1 control-label text-left">Min</label>
<div class="col-sm-5" ng-class="{ 'has-error': storidgeUpdateProfileForm.min_bandwidth.$invalid }">
<input type="number" class="form-control" ng-model="profile.MinBandwidth" name="min_bandwidth" ng-min="1" ng-max="5000" placeholder="1" required>
</div>
<label for="max_bandwidth" class="col-sm-1 control-label text-left">Max</label>
<div class="col-sm-5" ng-class="{ 'has-error': storidgeUpdateProfileForm.max_bandwidth.$invalid }">
<input type="number" class="form-control" ng-model="profile.MaxBandwidth" name="max_bandwidth" ng-min="1" ng-max="5000" placeholder="100" required>
</div>
</div>
<div class="form-group" ng-show="storidgeUpdateProfileForm.min_bandwidth.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeUpdateProfileForm.min_bandwidth.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Min bandwidth.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Min bandwidth: 1.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Min bandwidth: 5000.</p>
</div>
</div>
</div>
<div class="form-group" ng-show="storidgeUpdateProfileForm.max_bandwidth.$invalid">
<div class="col-sm-12 small text-danger">
<div ng-messages="storidgeUpdateProfileForm.max_bandwidth.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A value is required for Max bandwidth.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum value for Max bandwidth: 1.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum value for Max bandwidth: 5000.</p>
</div>
</div>
</div>
</div>
<!-- !bandwidth -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-click="update()" ng-disabled="state.updateInProgress || !storidgeUpdateProfileForm.$valid" button-spinner="state.updateInProgress">
<span ng-hide="state.updateInProgress">Update the profile</span>
<span ng-show="state.updateInProgress">Updating profile...</span>
</button>
<button type="button" class="btn btn-danger btn-sm" ng-click="delete()" ng-disabled="state.deleteInProgress" button-spinner="state.deleteInProgress">
<span ng-hide="state.deleteInProgress">Delete the profile</span>
<span ng-show="state.deleteInProgress">Deleting profile...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -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');
});
}]);

View File

@ -0,0 +1,123 @@
<rd-header>
<rd-header-title title="Storidge profiles">
<a data-toggle="tooltip" title="Refresh" ui-sref="storidge.profiles" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="storidge.cluster">Storidge</a> &gt; <a ui-sref="storidge.profiles">Profiles</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-md-12">
<rd-widget>
<rd-widget-header icon="fa-plus" title="Add a profile">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="profile_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="formValues.Name" id="profile_name" placeholder="e.g. myProfile">
</div>
</div>
<!-- !name-input -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: The profile will be created using the default properties.</span>
</div>
</div>
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-click="create()" ng-disabled="state.actionInProgress || !formValues.Name" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Create the profile</span>
<span ng-show="state.actionInProgress">Creating profile...</span>
</button>
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name" ui-sref="storidge.profiles.create({ profileName: formValues.Name })">Modify defaults...</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<storidge-profiles-datatable
title="Profiles" title-icon="fa-sticky-note-o"
dataset="profiles" table-key="storidge_profiles"
order-by="Name" show-text-filter="true"
remove-action="removeAction"
></storidge-profiles-datatable>
</div>
</div>
<!-- <div class="row">
<div class="col-md-12">
<rd-widget>
<rd-widget-header icon="fa-sticky-note-o" title="Profiles">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-md-12">
<div class="pull-left">
<button type="button" class="btn btn-danger" ng-click="removeProfiles()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a ng-click="order('Name')">
Name
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="profile in (state.filteredProfiles = (profiles | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="profile.Checked" ng-change="selectItem(profile)" /></td>
<td>
<a ui-sref="storidge.profiles.edit({id: profile.Name})">{{ profile.Name }}</a>
</td>
</tr>
<tr ng-if="!profiles">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="state.filteredProfiles.length == 0">
<td colspan="3" class="text-center text-muted">No profiles available.</td>
</tr>
</tbody>
</table>
<div ng-if="profiles" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div> -->

View File

@ -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');
});
}]);

View File

@ -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,

View File

@ -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;
}]);

View File

@ -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);
},

View File

@ -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();
})

View File

@ -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;
}

View File

@ -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']
};

View File

@ -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",

View File

@ -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

View File

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