mirror of https://github.com/portainer/portainer
feat(integrations): storidge evolution (#2711)
* feat(storidge): update storidge routes
* feat(storidge): add new fields on profile create/edit
* feat(storidge): add drives list and details view
* feat(storidge): add node details / cordon / uncordon / remove
* feat(storidge): add volume and snapshot details
* feat(storidge): add snapshot creation on volume details
* feat(storidge): add rescan drives button
* refactor(storidge): move add / remove / put in / put ouf maintenance buttons for cluster nodes
* style(storidge): change cluster / node icon color based on status
* feat(storidge): profiles can enable snapshots without interval + interval in minutes
* refactor(storidge): split cluster and node status badge filter
* fix(storidge): error on volume IOPS update
* fix(storidge): snapshot can now be created without comments
* feat(storidge): remove snapshots panels when volume snapshots are disabled
* fix(app): paginatedItemLimit now retrieved for datables extending GenericDatatableController
* fix(storidge): addDrive is called with the good parameters
* fix(storidge): update model and views for Storidge v2695
* refactor(storidge): webpack migration
* fix(storidge): display modifications + fix js errors
* feat(storidge): snapshots, profile and nodes evolution
* fix(storidge): values for InterfaceDriver on profile create/edit
* feat(storidge): v5 update without style (profile / statuses / volume)
* fix(storidge): description tables on the same view have now the same fixed offset
* fix(app): override rdash-ui select style
* Revert "fix(app): override rdash-ui select style"
This reverts commit e724833261
.
* feat(storidge): wip on update 6
* feat(storidge): update 6
* feat(storidge): update 6
* feat(storidge): update 6
* feat(storidge): update 7 - node details + cluster views
* fix(storidge): update 7 - profiles creation + volume details
* fix(storidge): update 7 - profile create/edit interface type
* feat(storidge): update 8 - add drive
* feat(storidge): update 8 - UI refactors + cluster availability
* fix(storidge): update 8 - revert cluster availability
* feat(storidge): update 8 - node availability on swarm overview
* feat(storidge): cluster condition badge
* fix(storidge): update 9 - move add storage button + api profile filesystem kv to obj
* feat(storidge): update 9 - disable add drive button when action is in progress
* fix(storidge): update 9 - add drive button will now change only for the concerned drive
* fix(storidge): update 10 - disable remove drive button when removal in progress
* fix(api): update Storidge proxy creation process
* refactor(api): update version number
* feat(extensions): fix an issue with Storidge API URL
* feat(storidge): force the use of a manager node
pull/2904/head
parent
17765d992e
commit
851607394c
|
@ -41,7 +41,7 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt
|
|||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Storidge extension not supported on this endpoint", portainer.ErrEndpointExtensionNotSupported}
|
||||
}
|
||||
|
||||
proxyExtensionKey := string(endpoint.ID) + "_" + string(portainer.StoridgeEndpointExtension)
|
||||
proxyExtensionKey := strconv.Itoa(endpointID) + "_" + strconv.Itoa(int(portainer.StoridgeEndpointExtension)) + "_" + storidgeExtension.URL
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetLegacyExtensionProxy(proxyExtensionKey)
|
||||
|
|
|
@ -50,9 +50,9 @@ func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Requ
|
|||
extensionType := portainer.EndpointExtensionType(payload.Type)
|
||||
|
||||
var extension *portainer.EndpointExtension
|
||||
for _, ext := range endpoint.Extensions {
|
||||
if ext.Type == extensionType {
|
||||
extension = &ext
|
||||
for idx := range endpoint.Extensions {
|
||||
if endpoint.Extensions[idx].Type == extensionType {
|
||||
extension = &endpoint.Extensions[idx]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string)
|
|||
}
|
||||
|
||||
proxy := manager.proxyFactory.newHTTPProxy(extensionURL)
|
||||
manager.extensionProxies.Set(key, proxy)
|
||||
manager.legacyExtensionProxies.Set(key, proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,13 @@
|
|||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Availability')">
|
||||
Availability
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Availability' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Availability' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -77,6 +84,7 @@
|
|||
<td>{{ item.EngineVersion }}</td>
|
||||
<td ng-if="$ctrl.showIpAddressColumn">{{ item.Addr }}</td>
|
||||
<td><span class="label label-{{ item.Status | nodestatusbadge }}">{{ item.Status }}</span></td>
|
||||
<td><span class="label label-{{ item.Availability | dockerNodeAvailabilityBadge }}">{{ item.Availability }}</span></td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="7" class="text-center text-muted">Loading...</td>
|
||||
|
|
|
@ -97,6 +97,18 @@ angular.module('portainer.docker')
|
|||
return 'success';
|
||||
};
|
||||
})
|
||||
.filter('dockerNodeAvailabilityBadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
if (text === 'pause') {
|
||||
return 'warning';
|
||||
}
|
||||
else if (text === 'drain') {
|
||||
return 'danger';
|
||||
}
|
||||
return 'success';
|
||||
};
|
||||
})
|
||||
.filter('trimcontainername', function () {
|
||||
'use strict';
|
||||
return function (name) {
|
||||
|
|
|
@ -9,6 +9,7 @@ angular.module('portainer.docker').factory('NodeService', [
|
|||
service.nodes = nodes;
|
||||
service.node = node;
|
||||
service.updateNode = updateNode;
|
||||
service.getActiveManager = getActiveManager;
|
||||
|
||||
function node(id) {
|
||||
var deferred = $q.defer();
|
||||
|
@ -45,6 +46,26 @@ angular.module('portainer.docker').factory('NodeService', [
|
|||
return Node.update({ id: node.Id, version: node.Version }, node).$promise;
|
||||
}
|
||||
|
||||
function getActiveManager() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
service.nodes()
|
||||
.then(function success(data) {
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
var node = data[i];
|
||||
if (node.Role === 'manager' && node.Availability === 'active' && node.Status === 'ready' && node.Addr !== '0.0.0.0') {
|
||||
deferred.resolve(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve nodes', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
]);
|
||||
|
|
|
@ -49,6 +49,31 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="isCioDriver">
|
||||
<div class="col-sm-12">
|
||||
<volume-storidge-info volume="storidgeVolume">
|
||||
</volume-storidge-info>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="isCioDriver">
|
||||
<div class="col-sm-12">
|
||||
<storidge-snapshot-creation volume-id="storidgeVolume.Vdisk" ng-if="storidgeVolume.SnapshotEnabled">
|
||||
</storidge-snapshot-creation>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="isCioDriver && storidgeVolume.SnapshotEnabled">
|
||||
<div class="col-sm-12">
|
||||
<storidge-snapshots-datatable
|
||||
title-text="Snapshots" title-icon="fa-camera"
|
||||
dataset="storidgeSnapshots" table-key="storidgeSnapshots"
|
||||
order-by="Id"
|
||||
remove-action="removeSnapshot">
|
||||
</storidge-snapshots-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- access-control-panel -->
|
||||
<por-access-control-panel
|
||||
ng-if="volume && applicationState.application.authentication"
|
||||
|
|
|
@ -1,6 +1,43 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('VolumeController', ['$scope', '$state', '$transition$', 'VolumeService', 'ContainerService', 'Notifications', 'HttpRequestHelper',
|
||||
function ($scope, $state, $transition$, VolumeService, ContainerService, Notifications, HttpRequestHelper) {
|
||||
.controller('VolumeController', ['$scope', '$state', '$transition$', '$q', 'ModalService', 'VolumeService', 'ContainerService', 'Notifications', 'HttpRequestHelper', 'StoridgeVolumeService', 'StoridgeSnapshotService',
|
||||
function ($scope, $state, $transition$, $q, ModalService, VolumeService, ContainerService, Notifications, HttpRequestHelper, StoridgeVolumeService, StoridgeSnapshotService) {
|
||||
|
||||
$scope.storidgeSnapshots = [];
|
||||
$scope.storidgeVolume = {};
|
||||
|
||||
$scope.removeSnapshot = function (selectedItems) {
|
||||
ModalService.confirm({
|
||||
title: 'Are you sure?',
|
||||
message: 'Do you want really want to remove this snapshot?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Remove',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
},
|
||||
callback: function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
var actionCount = selectedItems.length;
|
||||
angular.forEach(selectedItems, function (item) {
|
||||
StoridgeSnapshotService.remove(item.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('Snapshot successfully removed', item.Id);
|
||||
var index = $scope.storidgeSnapshots.indexOf(item);
|
||||
$scope.storidgeSnapshots.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove snapshot');
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeVolume = function removeVolume() {
|
||||
VolumeService.remove($scope.volume)
|
||||
|
@ -21,19 +58,41 @@ function ($scope, $state, $transition$, VolumeService, ContainerService, Notific
|
|||
|
||||
function initView() {
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName);
|
||||
|
||||
VolumeService.volume($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
var volume = data;
|
||||
$scope.volume = volume;
|
||||
var containerFilter = { volume: [volume.Id] };
|
||||
return ContainerService.containers(1, containerFilter);
|
||||
|
||||
$scope.isCioDriver = volume.Driver.includes('cio');
|
||||
if ($scope.isCioDriver) {
|
||||
return $q.all({
|
||||
containers: ContainerService.containers(1, containerFilter),
|
||||
storidgeVolume: StoridgeVolumeService.volume($transition$.params().id)
|
||||
});
|
||||
} else {
|
||||
return ContainerService.containers(1, containerFilter);
|
||||
}
|
||||
})
|
||||
.then(function success(data) {
|
||||
var containers = data.map(function(container) {
|
||||
var dataContainers = $scope.isCioDriver ? data.containers : data;
|
||||
|
||||
var containers = dataContainers.map(function(container) {
|
||||
container.volumeData = getVolumeDataFromContainer(container, $scope.volume.Id);
|
||||
return container;
|
||||
});
|
||||
$scope.containersUsingVolume = containers;
|
||||
|
||||
if ($scope.isCioDriver) {
|
||||
$scope.storidgeVolume = data.storidgeVolume;
|
||||
if ($scope.storidgeVolume.SnapshotEnabled) {
|
||||
return StoridgeSnapshotService.snapshots(data.storidgeVolume.Vdisk);
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.storidgeSnapshots = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve volume details');
|
||||
|
|
|
@ -32,6 +32,39 @@ angular.module('extension.storidge', [])
|
|||
}
|
||||
};
|
||||
|
||||
var drives = {
|
||||
name: 'storidge.drives',
|
||||
url: '/drives',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/drives/drives.html',
|
||||
controller: 'StoridgeDrivesController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var drive = {
|
||||
name: 'storidge.drives.drive',
|
||||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/drives/inspect/drive.html',
|
||||
controller: 'StoridgeDriveController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var snapshot = {
|
||||
name: 'docker.volumes.volume.snapshot',
|
||||
url: '/:snapshotId',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/snapshots/inspect/snapshot.html',
|
||||
controller: 'StoridgeSnapshotController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var profileCreation = {
|
||||
name: 'storidge.profiles.new',
|
||||
url: '/new',
|
||||
|
@ -57,6 +90,17 @@ angular.module('extension.storidge', [])
|
|||
}
|
||||
};
|
||||
|
||||
var node = {
|
||||
name: 'storidge.cluster.node',
|
||||
url: '/:name',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/nodes/inspect/node.html',
|
||||
controller: 'StoridgeNodeController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var monitor = {
|
||||
name: 'storidge.monitor',
|
||||
url: '/events',
|
||||
|
@ -69,9 +113,13 @@ angular.module('extension.storidge', [])
|
|||
};
|
||||
|
||||
$stateRegistryProvider.register(storidge);
|
||||
$stateRegistryProvider.register(drives);
|
||||
$stateRegistryProvider.register(drive);
|
||||
$stateRegistryProvider.register(snapshot);
|
||||
$stateRegistryProvider.register(profiles);
|
||||
$stateRegistryProvider.register(profile);
|
||||
$stateRegistryProvider.register(profileCreation);
|
||||
$stateRegistryProvider.register(cluster);
|
||||
$stateRegistryProvider.register(node);
|
||||
$stateRegistryProvider.register(monitor);
|
||||
}]);
|
||||
|
|
|
@ -2,7 +2,7 @@ angular.module('extension.storidge').component('storidgeClusterEventsDatatable',
|
|||
templateUrl: './storidgeClusterEventsDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
title: '@',
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
<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.titleText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.rescanAction()">
|
||||
<i class="fa fa-sync space-right" aria-hidden="true"></i>Rescan drives
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Id')">
|
||||
Id
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Node')">
|
||||
Node
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Node' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Node' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Device')">
|
||||
Device
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Device' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Device' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Size')">
|
||||
Size
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Size' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Size' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Use')">
|
||||
Use
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Use' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Use' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Type')">
|
||||
Type
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Status')">
|
||||
Status
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
Actions
|
||||
</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>
|
||||
<a ui-sref="storidge.drives.drive({id: item.Id})"> {{ item.Id }}</a>
|
||||
</td>
|
||||
<td>{{ item.Node }}</td>
|
||||
<td>{{ item.Device }}</td>
|
||||
<td>{{ item.Size }}</td>
|
||||
<td>{{ item.Use }}</td>
|
||||
<td>{{ item.Type }}</td>
|
||||
<td>
|
||||
<span class="label label-{{ item.Status|drivestatusbadge }}">{{ item.Status|capitalize }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<button ng-if="item.Status === 'available'" type="button" class="btn btn-sm btn-primary btn-datatable"
|
||||
ng-click="$ctrl.addAction(item, $index)" button-spinner="$ctrl.additionInProgress[$index]" ng-disabled="$ctrl.actionInProgress">
|
||||
<span ng-hide="$ctrl.additionInProgress[$index]"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add to storage pool</span>
|
||||
<span ng-show="$ctrl.additionInProgress[$index]">Addition in progress...</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="7" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td class="text-center text-muted">No drives 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>
|
|
@ -0,0 +1,17 @@
|
|||
angular.module('extension.storidge').component('storidgeDrivesDatatable', {
|
||||
templateUrl: './storidgeDrivesDatatable.html',
|
||||
controller: 'StoridgeDrivesDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
addAction: '<',
|
||||
rescanAction: '<',
|
||||
actionInProgress: '<',
|
||||
additionInProgress: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('StoridgeDrivesDatatableController', ['$scope', '$controller',
|
||||
function ($scope, $controller) {
|
||||
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||
|
||||
this.selectAll = function() {
|
||||
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
||||
var item = this.state.filteredDataSet[i];
|
||||
if (item.Status !== 'normal' && item.Checked !== this.state.selectAll) {
|
||||
item.Checked = this.state.selectAll;
|
||||
this.selectItem(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
|
@ -6,6 +6,25 @@
|
|||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<div class="row">
|
||||
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.addNodeAction()">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add node
|
||||
</button>
|
||||
<div style="margin-bottom: 0px;">
|
||||
<span ng-if="$ctrl.addInfo" class="text-muted small">
|
||||
To add a node to this cluster, run the following command on your new node
|
||||
<code>
|
||||
{{ $ctrl.addInfo }}
|
||||
</code>
|
||||
<span class="btn btn-primary btn-sm space-left" ng-click="$ctrl.copyAddNodeCommand()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</span>
|
||||
<span>
|
||||
<i id="copyNotification" class="fa fa-check green-icon" aria-hidden="true" style="margin-left: 7px; display: none;"></i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus>
|
||||
|
@ -46,11 +65,13 @@
|
|||
</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>
|
||||
<a ui-sref="storidge.cluster.node({name: item.Name})"> {{ item.Name }}</a>
|
||||
</td>
|
||||
<td>{{ item.IP }}</td>
|
||||
<td>{{ item.Role }}</td>
|
||||
<td>
|
||||
<i class="fa fa-heartbeat space-right green-icon"></i>
|
||||
<i class="fa fa-heartbeat space-right {{ item.Status | storidgeNodeStatusBadge }}"></i>
|
||||
{{ item.Status }}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
angular.module('extension.storidge').component('storidgeNodesDatatable', {
|
||||
templateUrl: './storidgeNodesDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
controller: 'StoridgeNodesDatatableController',
|
||||
bindings: {
|
||||
title: '@',
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
angular.module('extension.storidge')
|
||||
.controller('StoridgeNodesDatatableController', ['$scope', '$controller', 'clipboard', 'Notifications', 'StoridgeNodeService',
|
||||
function($scope, $controller, clipboard, Notifications, StoridgeNodeService) {
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
|
||||
|
||||
var ctrl = this;
|
||||
|
||||
this.addNodeAction = function() {
|
||||
StoridgeNodeService.add()
|
||||
.then(function sucess(data) {
|
||||
ctrl.addInfo = data.content;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve the "add node" command');
|
||||
});
|
||||
};
|
||||
|
||||
this.copyAddNodeCommand = function() {
|
||||
clipboard.copyText(ctrl.addInfo);
|
||||
$('#copyNotification').show();
|
||||
$('#copyNotification').fadeOut(2000);
|
||||
};
|
||||
}]);
|
|
@ -2,7 +2,7 @@ angular.module('extension.storidge').component('storidgeProfilesDatatable', {
|
|||
templateUrl: './storidgeProfilesDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
title: '@',
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plus" title-text="Create snapshot">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label for="description" class="col-sm-3 col-lg-2 control-label text-left">Description</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="description" ng-model="$ctrl.formValues.Description">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="$ctrl.state.actionInProgress"
|
||||
ng-click="$ctrl.createSnapshot()" button-spinner="$ctrl.state.actionInProgress">
|
||||
<span ng-hide="$ctrl.state.actionInProgress">Create snapshot</span>
|
||||
<span ng-show="$ctrl.state.actionInProgress">Creating snapshot...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
|
@ -0,0 +1,7 @@
|
|||
angular.module('portainer.docker').component('storidgeSnapshotCreation', {
|
||||
templateUrl: './storidgeSnapshotCreation.html',
|
||||
controller: 'StoridgeSnapshotCreationController',
|
||||
bindings: {
|
||||
volumeId: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('StoridgeSnapshotCreationController', ['StoridgeSnapshotService', 'Notifications', '$state',
|
||||
function (StoridgeSnapshotService, Notifications, $state) {
|
||||
var ctrl = this;
|
||||
|
||||
this.formValues = {};
|
||||
this.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
this.createSnapshot = function () {
|
||||
ctrl.state.actionInProgress = true;
|
||||
StoridgeSnapshotService.create(ctrl.volumeId, ctrl.formValues.Description)
|
||||
.then(function success() {
|
||||
Notifications.success('Success', 'Snapshot successfully created');
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create snapshot');
|
||||
})
|
||||
.finally(function final() {
|
||||
ctrl.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
}]);
|
|
@ -0,0 +1,95 @@
|
|||
<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.titleText }}
|
||||
</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-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" 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('Id')">
|
||||
Id
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Date')">
|
||||
Date
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Date' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Date' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Description')">
|
||||
Description
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Description' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Description' && $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)" ng-disabled="item.Status === 'normal'"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="docker.volumes.volume.snapshot({snapshotId: item.Id})"> {{ item.Id }}</a>
|
||||
</td>
|
||||
<td>{{ item.Date }}</td>
|
||||
<td>{{ item.Description }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="7" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="3" class="text-center text-muted">No snapshots 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>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('extension.storidge').component('storidgeSnapshotsDatatable', {
|
||||
templateUrl: './storidgeSnapshotsDatatable.html',
|
||||
controller: 'StoridgeSnapshotsDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('StoridgeSnapshotsDatatableController', ['$scope', '$controller',
|
||||
function ($scope, $controller) {
|
||||
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||
|
||||
}
|
||||
]);
|
|
@ -0,0 +1,198 @@
|
|||
<rd-widget>
|
||||
<rd-widget-header icon="fa-cube" title-text="Storidge details">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="$ctrl.updateVolume()" ng-if="!$ctrl.state.updateInProgress">
|
||||
<span>Update the volume</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="$ctrl.confirmUpdate()" ng-if="$ctrl.state.updateInProgress" button-spinner="$ctrl.state.isUpdating">
|
||||
<span ng-hide="$ctrl.state.isUpdating">Confirm</span>
|
||||
<span ng-show="$ctrl.state.isUpdating">Updating the volume...</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-click="$ctrl.cancelUpdate()" ng-if="$ctrl.state.updateInProgress" ng-disabled="$ctrl.state.isUpdating">
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding" ng-if="!$ctrl.state.updateInProgress">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ $ctrl.volume.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>UUID</td>
|
||||
<td>{{ $ctrl.volume.Uuid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Node</td>
|
||||
<td>{{ $ctrl.volume.Node }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Node ID</td>
|
||||
<td>{{ $ctrl.volume.NodeID }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Directory</td>
|
||||
<td>{{ $ctrl.volume.Directory }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Capacity</td>
|
||||
<td>{{ $ctrl.volume.Capacity }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Allocated</td>
|
||||
<td>{{ $ctrl.volume.Allocated }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IOPS Min</td>
|
||||
<td>{{ $ctrl.volume.IOPSMin }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IOPS Max</td>
|
||||
<td>{{ $ctrl.volume.IOPSMax }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bandwidth Min</td>
|
||||
<td>{{ $ctrl.volume.BandwidthMin }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bandwidth Max</td>
|
||||
<td>{{ $ctrl.volume.BandwidthMax }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Local Drive Only</td>
|
||||
<td>{{ $ctrl.volume.LocalDriveOnly }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provisioning</td>
|
||||
<td>{{ $ctrl.volume.Provisioning }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Redundancy</td>
|
||||
<td>{{ $ctrl.volume.Redundancy }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Vdisk</td>
|
||||
<td>{{ $ctrl.volume.Vdisk }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP</td>
|
||||
<td>{{ $ctrl.volume.IP}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Drive Type</td>
|
||||
<td>{{ $ctrl.volume.DriveType}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Encryption</td>
|
||||
<td>{{ $ctrl.volume.Encryption}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Snapshot Enabled</td>
|
||||
<td>{{ $ctrl.volume.SnapshotEnabled}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Snapshot Interval</td>
|
||||
<td>{{ $ctrl.volume.SnapshotInterval}} minute(s)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Max Snapshots</td>
|
||||
<td>{{ $ctrl.volume.SnapshotMax}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Filesystem</td>
|
||||
<td>{{ $ctrl.volume.Filesystem}}</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.volume.Labels">
|
||||
<td>Labels</td>
|
||||
<td>
|
||||
<table class="table table-bordered table-condensed">
|
||||
<tr ng-repeat="var in $ctrl.volume.Labels">
|
||||
<td>{{ var|key: '=' }}</td>
|
||||
<td>{{ var|value: '=' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body ng-if="$ctrl.state.updateInProgress">
|
||||
<form class="form-horizontal" name="storidgeUpdateVolumeForm">
|
||||
<!-- Node -->
|
||||
<div class="form-group">
|
||||
<label for="volume_node" class="col-sm-2 col-lg-1 control-label text-left">Node</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.formValues.Node" name="volume_node" placeholder="2">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Node -->
|
||||
<!-- Capacity -->
|
||||
<div class="form-group">
|
||||
<label for="volume_capacity" class="col-sm-2 col-lg-1 control-label text-left">Capacity</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.formValues.Capacity" name="volume_capacity" placeholder="2">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Capacity -->
|
||||
<!-- IOPS -->
|
||||
<div class="form-group">
|
||||
<label for="min_iops" class="col-sm-2 col-lg-1 control-label text-left">Min IOPS</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="number" class="form-control" ng-model="$ctrl.formValues.IOPSMin" name="min_iops" placeholder="100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="max_iops" class="col-sm-2 col-lg-1 control-label text-left">Max IOPS</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="number" class="form-control" ng-model="$ctrl.formValues.IOPSMax" name="max_iops" placeholder="2000">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !IOPS -->
|
||||
<!-- Bandwidth -->
|
||||
<div class="form-group">
|
||||
<label for="min_bandwidth" class="col-sm-2 col-lg-1 control-label text-left">Min Bandwidth</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="number" class="form-control" ng-model="$ctrl.formValues.BandwidthMin" name="min_bandwidth" placeholder="100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="max_bandwidth" class="col-sm-2 col-lg-1 control-label text-left">Max Bandwidth</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="number" class="form-control" ng-model="$ctrl.formValues.BandwidthMax" name="max_bandwidth" placeholder="2000">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Bandwidth -->
|
||||
<!-- labels -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Labels</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in $ctrl.formValues.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeLabel($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !labels -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
|
@ -0,0 +1,7 @@
|
|||
angular.module('portainer.docker').component('volumeStoridgeInfo', {
|
||||
templateUrl: './volumeStoridgeInfo.html',
|
||||
controller: 'VolumeStoridgeInfoController',
|
||||
bindings: {
|
||||
volume: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
angular.module('portainer.docker')
|
||||
.controller('VolumeStoridgeInfoController', ['$state', 'StoridgeVolumeService', 'Notifications',
|
||||
function ($state, StoridgeVolumeService, Notifications) {
|
||||
var ctrl = this;
|
||||
|
||||
this.state = {
|
||||
updateInProgress: false,
|
||||
isUpdating: false
|
||||
};
|
||||
|
||||
this.addLabel = function() {
|
||||
this.formValues.Labels.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
this.removeLabel = function(index) {
|
||||
this.formValues.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
this.initLabels = function() {
|
||||
var labels = this.volume.Labels;
|
||||
if (labels) {
|
||||
this.formValues.Labels = Object.keys(labels).map(function(key) {
|
||||
return { name:key, value:labels[key] };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.updateVolume = function() {
|
||||
this.state.updateInProgress = true;
|
||||
this.formValues = {
|
||||
IOPSMin: this.volume.IOPSMin,
|
||||
IOPSMax: this.volume.IOPSMax,
|
||||
Node: this.volume.Node,
|
||||
Capacity: this.volume.Capacity,
|
||||
BandwidthMin: this.volume.BandwidthMin,
|
||||
BandwidthMax: this.volume.BandwidthMax,
|
||||
Labels: []
|
||||
};
|
||||
this.initLabels();
|
||||
};
|
||||
|
||||
this.cancelUpdate = function() {
|
||||
this.state.updateInProgress = false;
|
||||
this.formValues = {};
|
||||
};
|
||||
|
||||
this.prepareLabels = function(volume) {
|
||||
var labels = {};
|
||||
this.formValues.Labels.forEach(function (label) {
|
||||
if (label.name && label.value) {
|
||||
labels[label.name] = label.value;
|
||||
}
|
||||
});
|
||||
volume.Labels = labels;
|
||||
};
|
||||
|
||||
this.prepareVolume = function() {
|
||||
var volume = angular.copy(this.formValues);
|
||||
var data = this.volume;
|
||||
|
||||
if (volume.Node === data.Node || !volume.Node) {
|
||||
delete volume.Node;
|
||||
}
|
||||
if (volume.Capacity === data.Capacity || !volume.Capacity) {
|
||||
delete volume.Capacity;
|
||||
}
|
||||
if (volume.IOPSMin === data.IOPSMin || !volume.IOPSMin) {
|
||||
delete volume.IOPSMin;
|
||||
} else {
|
||||
volume.IOPSMin = volume.IOPSMin.toString();
|
||||
}
|
||||
if (volume.IOPSMax === data.IOPSMax || !volume.IOPSMax) {
|
||||
delete volume.IOPSMax;
|
||||
} else {
|
||||
volume.IOPSMax = volume.IOPSMax.toString();
|
||||
}
|
||||
if (volume.BandwidthMin === data.BandwidthMin || !volume.BandwidthMin) {
|
||||
delete volume.BandwidthMin;
|
||||
}
|
||||
if (volume.BandwidthMax === data.BandwidthMax || !volume.BandwidthMax) {
|
||||
delete volume.BandwidthMax;
|
||||
}
|
||||
this.prepareLabels(volume);
|
||||
return volume;
|
||||
};
|
||||
|
||||
this.confirmUpdate = function() {
|
||||
this.state.isUpdating = true;
|
||||
|
||||
var volume = this.prepareVolume();
|
||||
volume.Name = this.volume.Name;
|
||||
StoridgeVolumeService.update(volume)
|
||||
.then(function success() {
|
||||
Notifications.success('Volume successfully updated');
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update volume');
|
||||
ctrl.state.isUpdating = false;
|
||||
});
|
||||
};
|
||||
|
||||
}]);
|
|
@ -0,0 +1,51 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('extension.storidge')
|
||||
.filter('drivestatusbadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
var status = text ? _.toLower(text) : '';
|
||||
if (status === 'available') {
|
||||
return 'info';
|
||||
} else if (status === 'faulty') {
|
||||
return 'danger';
|
||||
}
|
||||
return 'success';
|
||||
};
|
||||
})
|
||||
.filter('storidgeNodeStatusBadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
var status = text ? _.toLower(text) : '';
|
||||
if (status === 'cordoned' || status === 'maintenance') {
|
||||
return 'orange-icon';
|
||||
} else if (status === 'leaving' || status === 'failed') {
|
||||
return 'red-icon'
|
||||
}
|
||||
return 'green-icon';
|
||||
};
|
||||
})
|
||||
.filter('storidgeClusterConditionBadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
var status = text ? _.toLower(text) : '';
|
||||
if (status === 'alert') {
|
||||
return 'red-icon';
|
||||
} else if (status === 'warning') {
|
||||
return 'orange-icon';
|
||||
}
|
||||
return 'green-icon';
|
||||
};
|
||||
}).filter('bytes', function() {
|
||||
return function(bytes, precision) {
|
||||
bytes = parseFloat(bytes);
|
||||
if (isNaN(bytes) || !isFinite(bytes)) return '-';
|
||||
if (!precision) precision = 1;
|
||||
var units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
||||
var number = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
if (bytes === 0) {
|
||||
return ('0 B');
|
||||
}
|
||||
return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number];
|
||||
}
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
export function StoridgeDriveModel(data) {
|
||||
this.Id = data.driveid;
|
||||
this.Node = data.node;
|
||||
this.Use = data.use;
|
||||
this.Status = data.drivestatus.toLowerCase();
|
||||
this.Size = data.size;
|
||||
this.Type = data.type;
|
||||
this.Device = data.device;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
export function StoridgeInfoModel(data) {
|
||||
this.Domain = data.domain;
|
||||
this.Nodes = data.nodes;
|
||||
this.Status = data.status;
|
||||
this.Condition = data.condition;
|
||||
this.ProvisionedBandwidth = data.provisionedBandwidth;
|
||||
this.UsedBandwidth = data.usedBandwidth;
|
||||
this.FreeBandwidth = data.freeBandwidth;
|
||||
|
|
|
@ -4,3 +4,28 @@ export function StoridgeNodeModel(name, data) {
|
|||
this.Role = data.role;
|
||||
this.Status = data.status;
|
||||
}
|
||||
|
||||
export function StoridgeNodeDetailedModel(name, properties) {
|
||||
this.Name = name;
|
||||
this.Condition = properties.condition;
|
||||
this.Domain = properties.domain;
|
||||
this.DomainID = properties.domainID;
|
||||
this.FreeBandwidth = properties.freeBandwidth;
|
||||
this.FreeCapacity = properties.freeCapacity;
|
||||
this.FreeIOPS = properties.freeIOPS;
|
||||
this.Hdds = properties.hdds;
|
||||
this.MetadataVersion = properties.metadataVersion;
|
||||
this.Nodes = properties.nodes;
|
||||
this.ProvisionedBandwidth = properties.provisionedBandwidth;
|
||||
this.ProvisionedCapacity = properties.provisionedCapacity;
|
||||
this.ProvisionedIOPS = properties.provisionedIOPS;
|
||||
this.Ssds = properties.ssds;
|
||||
this.Status = properties.status;
|
||||
this.TotalBandwidth = properties.totalBandwidth;
|
||||
this.TotalCapacity = properties.totalCapacity;
|
||||
this.TotalIOPS = properties.totalIOPS;
|
||||
this.UsedBandwidth = properties.usedBandwidth;
|
||||
this.UsedCapacity = properties.usedCapacity;
|
||||
this.UsedIOPS = properties.usedIOPS;
|
||||
this.Vdisks = properties.vdisks;
|
||||
}
|
|
@ -8,6 +8,16 @@ export function StoridgeProfileDefaultModel() {
|
|||
this.MaxIOPS = 2000;
|
||||
this.MinBandwidth = 1;
|
||||
this.MaxBandwidth = 100;
|
||||
this.Filesystem = 'btrfs';
|
||||
this.SnapshotEnabled = false;
|
||||
this.SnapshotInterval = 1440;
|
||||
this.SnapshotMax = 1;
|
||||
this.EncryptionEnabled = false;
|
||||
this.InterfaceType = '';
|
||||
this.InterfaceDriver = '';
|
||||
this.InterfaceNetwork = '';
|
||||
this.InterfaceConf = '';
|
||||
this.Labels = [];
|
||||
}
|
||||
|
||||
export function StoridgeProfileListModel(data) {
|
||||
|
@ -32,6 +42,40 @@ export function StoridgeProfileModel(name, data) {
|
|||
this.MinBandwidth = data.bandwidth.min;
|
||||
this.MaxBandwidth = data.bandwidth.max;
|
||||
}
|
||||
|
||||
if (data.filesystem) {
|
||||
this.Filesystem = data.filesystem.type;
|
||||
}
|
||||
// this.Filesystem = data.filesystem;
|
||||
|
||||
var service = data.service;
|
||||
|
||||
if (service.snapshot) {
|
||||
this.SnapshotEnabled = service.snapshot.enabled;
|
||||
this.SnapshotInterval = service.snapshot.interval;
|
||||
this.SnapshotMax = service.snapshot.max;
|
||||
} else {
|
||||
this.SnapshotEnabled = false;
|
||||
}
|
||||
|
||||
if (service.encryption) {
|
||||
this.EncryptionEnabled = service.encryption.enabled;
|
||||
} else {
|
||||
this.EncryptionEnabled = false;
|
||||
}
|
||||
|
||||
if (data.interface) {
|
||||
this.InterfaceType = data.interface.type;
|
||||
this.InterfaceDriver = data.interface.driver;
|
||||
this.InterfaceNetwork = data.interface.network;
|
||||
this.InterfaceConf = data.interface.conf;
|
||||
}
|
||||
|
||||
if (data.label) {
|
||||
this.Labels = data.label;
|
||||
} else {
|
||||
this.Labels = [];
|
||||
}
|
||||
}
|
||||
|
||||
export function StoridgeCreateProfileRequest(model) {
|
||||
|
@ -54,4 +98,36 @@ export function StoridgeCreateProfileRequest(model) {
|
|||
max: model.MaxBandwidth
|
||||
};
|
||||
}
|
||||
|
||||
this.filesystem = {
|
||||
type: model.Filesystem
|
||||
};
|
||||
|
||||
var service = {};
|
||||
|
||||
service.snapshot = {
|
||||
enabled: model.SnapshotEnabled
|
||||
};
|
||||
if (model.SnapshotEnabled) {
|
||||
service.snapshot.interval = model.SnapshotInterval;
|
||||
service.snapshot.max = model.SnapshotMax;
|
||||
}
|
||||
|
||||
service.encryption = {
|
||||
enabled: model.EncryptionEnabled
|
||||
};
|
||||
|
||||
this.service = service;
|
||||
|
||||
this.interface = {
|
||||
driver: model.InterfaceDriver,
|
||||
network: model.InterfaceNetwork,
|
||||
conf: model.InterfaceConf
|
||||
};
|
||||
|
||||
if (model.InterfaceType) {
|
||||
this.interface.type = model.InterfaceType;
|
||||
}
|
||||
|
||||
this.label = model.Labels;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export function StoridgeSnapshotModel(data) {
|
||||
this.Id = data.identifier;
|
||||
this.Date = data.date;
|
||||
this.Description = data.description;
|
||||
this.SourceID = data.sourceid;
|
||||
this.Type = data.type;
|
||||
this.Directory = data.directory;
|
||||
this.Source = data.source;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
export function StoridgeVolumeModel(data) {
|
||||
this.Allocated = data.allocated;
|
||||
this.Capacity = data.capacity;
|
||||
this.Directory = data.directory;
|
||||
this.IOPSMax = data.maximumIOPS;
|
||||
this.IOPSMin = data.minimumIOPS;
|
||||
this.BandwidthMin = data.minimumBandwidth;
|
||||
this.BandwidthMax = data.maximumBandwidth;
|
||||
this.LocalDriveOnly = data.localDriveOnly;
|
||||
this.Name = data.name;
|
||||
this.Node = data.node;
|
||||
this.NodeID = data.nodeid;
|
||||
this.Provisioning = data.provisioning;
|
||||
this.Redundancy = data.redundancy;
|
||||
this.Uuid = data.uuid;
|
||||
this.Vdisk = data.vdisk;
|
||||
this.Labels = data.labels;
|
||||
|
||||
this.IP = data.ipaddr;
|
||||
this.DriveType = data.driveType;
|
||||
this.Encryption = data.encryption;
|
||||
this.SnapshotEnabled = data.snapshot;
|
||||
this.SnapshotInterval = data.snapInterval;
|
||||
this.SnapshotMax = data.maximumSnapshots;
|
||||
this.Filesystem = data.filesystem;
|
||||
}
|
||||
|
||||
export function StoridgeVolumeUpdateModel(data) {
|
||||
this.name = data.Name;
|
||||
this.opts = {
|
||||
node: data.Node,
|
||||
nodeid: data.NodeID,
|
||||
capacity: data.Capacity,
|
||||
iopsmin: data.IOPSMin,
|
||||
iopsmax: data.IOPSMax,
|
||||
bandwidthmin: data.BandwidthMin,
|
||||
bandwidthmax: data.BandwidthMax
|
||||
};
|
||||
this.labels = data.Labels;
|
||||
}
|
|
@ -5,16 +5,41 @@ angular.module('extension.storidge')
|
|||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
||||
rebootCluster: { method: 'POST', params: { resource: 'cluster', action: 'reboot' } },
|
||||
shutdownCluster: { method: 'POST', params: { resource: 'cluster', action: 'shutdown' } },
|
||||
queryEvents: { method: 'GET', params: { resource: 'events' }, timeout: 4500, ignoreLoadingBar: true, isArray: true },
|
||||
getVersion: { method: 'GET', params: { resource: 'version' } },
|
||||
getInfo: { method: 'GET', params: { resource: 'info' }, timeout: 4500, ignoreLoadingBar: true },
|
||||
rebootCluster: { method: 'POST', params: { resource: 'clusters', action: 'reboot' } },
|
||||
shutdownCluster: { method: 'POST', params: { resource: 'clusters', action: 'shutdown' } },
|
||||
queryEvents: { method: 'GET', params: { resource: 'clusters', action: 'events' }, timeout: 4500, ignoreLoadingBar: true, isArray: true },
|
||||
getVersion: { method: 'GET', params: { resource: 'clusters', action: 'version' } },
|
||||
getInfo: { method: 'GET', params: { resource: 'clusters', action: 'info' }, timeout: 4500, ignoreLoadingBar: true },
|
||||
|
||||
queryNodes: { method: 'GET', params: { resource: 'nodes' } },
|
||||
getNode: { method: 'GET', params: { resource: 'nodes', id: '@id' } },
|
||||
addNode: { method: 'POST', params: { resource: 'nodes' } },
|
||||
removeNode: { method: 'DELETE', params: { resource: 'nodes', id: '@id' } },
|
||||
cordonNode: { method: 'POST', params : { resource: 'nodes', action:'cordon', id: '@id' } },
|
||||
uncordonNode: { method: 'POST', params : { resource: 'nodes', action: 'uncordon', id:'@id' } },
|
||||
|
||||
queryProfiles: { method: 'GET', params: { resource: 'profiles' } },
|
||||
getProfile: { method: 'GET', params: { resource: 'profiles' } },
|
||||
createProfile: { method: 'POST', params: { resource: 'profiles' } },
|
||||
updateProfile: { method: 'PUT', params: { resource: 'profiles', id: '@name' } },
|
||||
deleteProfile: { method: 'DELETE', params: { resource: 'profiles' } }
|
||||
deleteProfile: { method: 'DELETE', params: { resource: 'profiles' } },
|
||||
|
||||
queryDrives: { method: 'GET', params: { resource: 'drives' } },
|
||||
getDrive: { method: 'GET', params: { resource: 'drives', id: '@id' } },
|
||||
addDrive: { method: 'POST', params: { resource: 'drives' } },
|
||||
removeDrive: { method: 'DELETE', params: { resource: 'drives', id: '@id' } },
|
||||
rescanDrives: { method: 'POST', params: { resource: 'drives', action: 'rescan' } },
|
||||
|
||||
queryVolumes: { method: 'GET', params: { resource: 'volumes' } },
|
||||
createVolume: { method: 'POST', params: { resource: 'volumes' } },
|
||||
getVolume: { method: 'GET', params: { resource: 'volumes', id: '@id' } },
|
||||
updateVolume: { method: 'POST', params: { resource: 'volumes', id: '@name' } },
|
||||
removeVolume: { method: 'DELETE', params: { resource: 'volumes' , id: '@id' } },
|
||||
|
||||
querySnapshots: { method: 'GET', params: { resource: 'volumes', id: '@id', action: 'snapshots' } },
|
||||
createSnapshot: { method: 'POST', params: { resource: 'volumes', id: '@id', action: 'snapshot' } },
|
||||
getSnapshot: { method: 'GET', params: { resource: 'snapshots', id: '@id' } },
|
||||
removeSnapshot: { method: 'DELETE', params: { resource: 'snapshots', id: '@id'} }
|
||||
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import { StoridgeDriveModel } from '../models/drive';
|
||||
|
||||
angular.module('extension.storidge')
|
||||
.factory('StoridgeDriveService', ['$q', 'Storidge', function StoridgeDriveServiceFactory($q, Storidge) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.drives = function () {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Storidge.queryDrives().$promise
|
||||
.then(function success(data) {
|
||||
var driveData = data.drives;
|
||||
var drives = driveData.map(function (drive) {
|
||||
return new StoridgeDriveModel(drive);
|
||||
});
|
||||
|
||||
deferred.resolve(drives);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge drives', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.drive = function (id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Storidge.getDrive({ id: id }).$promise
|
||||
.then(function success(data) {
|
||||
var drive = new StoridgeDriveModel(data);
|
||||
Storidge.getNode({ id: data.nodeid }).$promise
|
||||
.then(function (data) {
|
||||
drive.Node = data.name;
|
||||
deferred.resolve(drive);
|
||||
});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge drive', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.add = function (device, node) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Storidge.addDrive({ device: device, node: node }).$promise
|
||||
.then(function success() {
|
||||
deferred.resolve();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to add Storidge drive', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.remove = function (id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Storidge.removeDrive({ id: id }).$promise
|
||||
.then(function success() {
|
||||
deferred.resolve();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to remove Storidge drive', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.rescan = function () {
|
||||
return Storidge.rescanDrives().$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -1,4 +1,4 @@
|
|||
import { StoridgeNodeModel } from '../models/node';
|
||||
import { StoridgeNodeModel, StoridgeNodeDetailedModel } from '../models/node';
|
||||
|
||||
angular.module('extension.storidge')
|
||||
.factory('StoridgeNodeService', ['$q', 'Storidge', function StoridgeNodeServiceFactory($q, Storidge) {
|
||||
|
@ -22,11 +22,42 @@ angular.module('extension.storidge')
|
|||
deferred.resolve(nodes);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge profiles', err: err });
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge nodes', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.node = function (id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Storidge.getNode({id:id}).$promise
|
||||
.then(function success(data) {
|
||||
var node = new StoridgeNodeDetailedModel(data.name, data.properties);
|
||||
deferred.resolve(node);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge node', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.add = function () {
|
||||
return Storidge.addNode().$promise;
|
||||
};
|
||||
|
||||
service.cordon = function (id) {
|
||||
return Storidge.cordonNode({id: id}).$promise;
|
||||
};
|
||||
|
||||
service.uncordon = function (id) {
|
||||
return Storidge.uncordonNode({id: id}).$promise;
|
||||
};
|
||||
|
||||
service.remove = function (id) {
|
||||
return Storidge.removeNode({id:id}).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import { StoridgeSnapshotModel } from '../models/snapshot'
|
||||
|
||||
angular.module('extension.storidge')
|
||||
.factory('StoridgeSnapshotService', ['$q', 'Storidge', function StoridgeSnapshotServiceFactory($q, Storidge) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.snapshots = snapshots;
|
||||
service.snapshot = snapshot;
|
||||
service.create = create;
|
||||
service.remove = remove;
|
||||
|
||||
function snapshots(volumeId) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Storidge.querySnapshots({id: volumeId}).$promise
|
||||
.then(function success(data) {
|
||||
var snapshotsData = data.snapshots;
|
||||
let snapshotsArray = [];
|
||||
for (const key in snapshotsData) {
|
||||
if (snapshotsData.hasOwnProperty(key)) {
|
||||
snapshotsArray.push(snapshotsData[key]);
|
||||
}
|
||||
}
|
||||
var snapshots = snapshotsArray.map(function (snapshot) {
|
||||
return new StoridgeSnapshotModel(snapshot);
|
||||
});
|
||||
deferred.resolve(snapshots);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge snapshots', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function snapshot(id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Storidge.getSnapshot({id:id}).$promise
|
||||
.then(function success(data) {
|
||||
var snapshot = new StoridgeSnapshotModel(data.snapshot);
|
||||
deferred.resolve(snapshot);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge snapshot', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function create(volumeId, description) {
|
||||
var deferred = $q.defer();
|
||||
Storidge.createSnapshot({id: volumeId, opts: {description: description}}).$promise
|
||||
.then(function success(data) {
|
||||
deferred.resolve(data);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to create Storidge volume snapshot', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function remove(id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Storidge.removeSnapshot({ id: id }).$promise
|
||||
.then(function success() {
|
||||
deferred.resolve();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to remove Storidge volume snapshot', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -0,0 +1,38 @@
|
|||
import { StoridgeVolumeModel, StoridgeVolumeUpdateModel } from '../models/volume';
|
||||
|
||||
angular.module('extension.storidge')
|
||||
.factory('StoridgeVolumeService', ['$q', 'Storidge', function StoridgeVolumeServiceFactory($q, Storidge) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.volume = function(id) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Storidge.getVolume({id:id}).$promise
|
||||
.then(function success(data) {
|
||||
var volume = new StoridgeVolumeModel(data);
|
||||
deferred.resolve(volume);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve Storidge volume', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.update = function(data) {
|
||||
var deferred = $q.defer();
|
||||
var volume = new StoridgeVolumeUpdateModel(data);
|
||||
Storidge.updateVolume(volume).$promise
|
||||
.then(function success(data) {
|
||||
deferred.resolve(data);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to update Storidge volume', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -21,8 +21,10 @@
|
|||
<td>{{ clusterInfo.Domain }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><i class="fa fa-heartbeat space-right green-icon"></i> {{ clusterInfo.Status }}</td>
|
||||
<td>Condition</td>
|
||||
<td><i class="fa fa-heartbeat space-right {{ clusterInfo.Condition | storidgeClusterConditionBadge }}"></i>
|
||||
{{ clusterInfo.Condition }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
|
@ -57,89 +59,7 @@
|
|||
<storidge-nodes-datatable
|
||||
title-text="Storage nodes" title-icon="fa-object-group"
|
||||
dataset="clusterNodes" table-key="storidge_nodes"
|
||||
order-by="Name"
|
||||
order-by="Name"
|
||||
></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-text="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> -->
|
||||
|
|
|
@ -24,6 +24,16 @@ function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNod
|
|||
});
|
||||
};
|
||||
|
||||
function rebootCluster() {
|
||||
$scope.state.rebootInProgress = true;
|
||||
StoridgeClusterService.reboot()
|
||||
.finally(function final() {
|
||||
$scope.state.rebootInProgress = false;
|
||||
Notifications.success('Cluster successfully rebooted');
|
||||
$state.reload();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.shutdownCluster = function() {
|
||||
ModalService.confirm({
|
||||
title: 'Are you sure?',
|
||||
|
@ -51,16 +61,6 @@ function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNod
|
|||
});
|
||||
}
|
||||
|
||||
function rebootCluster() {
|
||||
$scope.state.rebootInProgress = true;
|
||||
StoridgeClusterService.reboot()
|
||||
.finally(function final() {
|
||||
$scope.state.rebootInProgress = false;
|
||||
Notifications.success('Cluster successfully rebooted');
|
||||
$state.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
$q.all({
|
||||
info: StoridgeClusterService.info(),
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Storidge drives">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="storidge.drives" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="storidge.cluster">Storidge</a> > <a ui-sref="storidge.drives">Drives</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<storidge-drives-datatable
|
||||
title-text="Drives" title-icon="fa-hdd"
|
||||
dataset="drives" table-key="storidge_drives"
|
||||
order-by="Id"
|
||||
rescan-action="rescanAction"
|
||||
add-action="addAction"
|
||||
action-in-progress="state.actionInProgress"
|
||||
addition-in-progress="state.additionInProgress"
|
||||
></storidge-drives-datatable>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,48 @@
|
|||
angular.module('extension.storidge')
|
||||
.controller('StoridgeDrivesController', ['$scope', '$state', 'Notifications', 'StoridgeDriveService',
|
||||
function ($scope, $state, Notifications, StoridgeDriveService) {
|
||||
|
||||
$scope.state = {
|
||||
additionInProgress: [],
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.addAction = function (drive, idx) {
|
||||
$scope.state.additionInProgress[idx] = true;
|
||||
$scope.state.actionInProgress = true;
|
||||
StoridgeDriveService.add(drive.Device, drive.Node)
|
||||
.then(function success() {
|
||||
Notifications.success('Drive ' + drive.Device + ' successfully added on node ' + drive.Node);
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to add drive');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.additionInProgress[idx] = false;
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.rescanAction = function () {
|
||||
StoridgeDriveService.rescan()
|
||||
.then(function sucess() {
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to scan drives');
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
StoridgeDriveService.drives()
|
||||
.then(function success(data) {
|
||||
$scope.drives = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve drives');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
|
@ -0,0 +1,56 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Drive details"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="storidge.cluster">Storidge</a> > <a ui-sref="storidge.drives">Drives</a> > <a ui-sref="storidge.drives.drive({id: drive.Id})">{{ drive.Id }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-hdd" title-text="Drive details "></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>
|
||||
{{ drive.Id }}
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeDrive()" ng-if="drive.Status === 'faulty'" button-spinner="actionInProgress" ng-disabled="actionInProgress">
|
||||
<span ng-hide="actionInProgress"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove from storage pool</span>
|
||||
<span ng-show="actionInProgress">Removal in progress...</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Node</td>
|
||||
<td>{{ drive.Node }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>{{ drive.Device }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Size</td>
|
||||
<td>{{ drive.Size }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Use</td>
|
||||
<td>{{ drive.Use }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ drive.Type }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
<span class="label label-{{ drive.Status|drivestatusbadge }}">{{ drive.Status|capitalize }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,49 @@
|
|||
angular.module('extension.storidge')
|
||||
.controller('StoridgeDriveController', ['$scope', '$state', '$transition$', 'Notifications', 'ModalService', 'StoridgeDriveService',
|
||||
function ($scope, $state, $transition$, Notifications, ModalService, StoridgeDriveService) {
|
||||
|
||||
$scope.actionInProgress = false;
|
||||
|
||||
$scope.removeDrive = function () {
|
||||
ModalService.confirm({
|
||||
title: 'Are you sure?',
|
||||
message: 'Do you want really want to remove this drive from the storage pool?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Remove',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
},
|
||||
callback: function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
$scope.actionInProgress = true;
|
||||
StoridgeDriveService.remove($scope.drive.Id)
|
||||
.then(function () {
|
||||
Notifications.success('Success', 'Drive removed from storage pool');
|
||||
$state.go('storidge.drives', {}, { reload:true });
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove drive from storage pool');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
$scope.id = $transition$.params().id;
|
||||
|
||||
StoridgeDriveService.drive($scope.id)
|
||||
.then(function success(data) {
|
||||
$scope.drive = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve drive details');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
}]);
|
|
@ -0,0 +1,170 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Node details"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="storidge.cluster">Storidge</a> > <a
|
||||
ui-sref="storidge.cluster.node({id: node.Name})">{{ node.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title-text="Node details">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
ng-disabled="(node.Status | lowercase) !== 'normal'" ng-click="removeNodeAction()">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove node
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary"
|
||||
ng-disabled="(node.Status | lowercase) !== 'normal'" ng-click="cordonNodeAction()">
|
||||
<i class="fas fa-wrench space-right" aria-hidden="true"></i>Enter maintenance mode
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary"
|
||||
ng-disabled="(node.Status | lowercase) !== 'cordoned'" ng-click="uncordonNodeAction()">
|
||||
<i class="fa fa-power-off space-right" aria-hidden="true"></i>Exit maintenance mode
|
||||
</button>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table description-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ node.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Domain</td>
|
||||
<td>{{ node.Domain }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Domain ID</td>
|
||||
<td>{{ node.DomainID }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Node status</td>
|
||||
<td>
|
||||
<i class="fa fa-heartbeat space-right {{ node.Status | storidgeNodeStatusBadge }}"></i>
|
||||
{{ node.Status }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Operating condition</td>
|
||||
<td>
|
||||
<i class="fa fa-heartbeat space-right {{ node.Condition | storidgeClusterConditionBadge }}"></i>
|
||||
{{ node.Condition }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Metadata version</td>
|
||||
<td>{{ node.MetadataVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nodes</td>
|
||||
<td>{{ node.Nodes }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>HDDs</td>
|
||||
<td>{{ node.Hdds }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SSDs</td>
|
||||
<td>{{ node.Ssds }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VDisks</td>
|
||||
<td>{{ node.Vdisks }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title-text="Bandwidth details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table description-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Free</td>
|
||||
<td>{{ node.FreeBandwidth }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Used</td>
|
||||
<td>{{ node.UsedBandwidth }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provisioned</td>
|
||||
<td>{{ node.ProvisionedBandwidth }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total</td>
|
||||
<td>{{ node.TotalBandwidth }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title-text="Capacity details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table description-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Free</td>
|
||||
<td>{{ node.FreeCapacity | bytes }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Used</td>
|
||||
<td>{{ node.UsedCapacity | bytes }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provisioned</td>
|
||||
<td>{{ node.ProvisionedCapacity | bytes }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total</td>
|
||||
<td>{{ node.TotalCapacity | bytes }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title-text="IOPS details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table description-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Free</td>
|
||||
<td>{{ node.FreeIOPS }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Used</td>
|
||||
<td>{{ node.UsedIOPS }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Provisioned</td>
|
||||
<td>{{ node.ProvisionedIOPS }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total</td>
|
||||
<td>{{ node.TotalIOPS }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,107 @@
|
|||
angular.module('extension.storidge')
|
||||
.controller('StoridgeNodeController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeNodeService', 'ModalService',
|
||||
function ($scope, $state, $transition$, Notifications, StoridgeNodeService, ModalService) {
|
||||
|
||||
$scope.removeNodeAction = function(selectedItems) {
|
||||
ModalService.confirm({
|
||||
title: 'Are you sure?',
|
||||
message: 'Do you want really want to remove the node from the cluster?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Remove',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
},
|
||||
callback: function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
remove(selectedItems);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function remove() {
|
||||
StoridgeNodeService.remove($scope.node.Name)
|
||||
.then(function success() {
|
||||
Notifications.success('Node successfully removed', $scope.node.Name);
|
||||
$state.go('storidge.cluster');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove node');
|
||||
});
|
||||
}
|
||||
|
||||
$scope.cordonNodeAction = function(selectedItems) {
|
||||
ModalService.confirm({
|
||||
title: 'Are you sure?',
|
||||
message: 'Do you want really want to put the node in maintenance mode?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Enter maintenance',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
},
|
||||
callback: function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
cordonNode(selectedItems);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function cordonNode() {
|
||||
StoridgeNodeService.cordon($scope.node.Name)
|
||||
.then(function success() {
|
||||
Notifications.success('Node successfully put in maintenance');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to put node in maintenance mode');
|
||||
})
|
||||
.finally(function final() {
|
||||
$state.reload();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.uncordonNodeAction = function(selectedItems) {
|
||||
ModalService.confirm({
|
||||
title: 'Are you sure?',
|
||||
message: 'Do you want really want to bring the nodes out of maintenance mode?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Exit maintenance',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
},
|
||||
callback: function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
uncordonNode(selectedItems);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function uncordonNode() {
|
||||
StoridgeNodeService.uncordon($scope.node.Name)
|
||||
.then(function success() {
|
||||
Notifications.success('Node successfully bringed back');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to put node out of maintenance mode');
|
||||
})
|
||||
.finally(function final() {
|
||||
$state.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
$scope.name = $transition$.params().name;
|
||||
|
||||
StoridgeNodeService.node($scope.name)
|
||||
.then(function success(data) {
|
||||
$scope.node = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve node details');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
}]);
|
|
@ -1,9 +1,14 @@
|
|||
import _ from 'lodash-es';
|
||||
import { StoridgeProfileDefaultModel } from '../../../models/profile';
|
||||
|
||||
angular.module('extension.storidge')
|
||||
.controller('StoridgeCreateProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService',
|
||||
function ($scope, $state, $transition$, Notifications, StoridgeProfileService) {
|
||||
|
||||
$scope.formValues = {
|
||||
Labels: []
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
NoLimit: true,
|
||||
LimitIOPS: false,
|
||||
|
@ -17,6 +22,24 @@ function ($scope, $state, $transition$, Notifications, StoridgeProfileService) {
|
|||
{ value: 3, label: '3-copy' }
|
||||
];
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.formValues.Labels.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.formValues.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
function prepareLabels(profile) {
|
||||
var labels = {};
|
||||
$scope.formValues.Labels.forEach(function (label) {
|
||||
if (label.name && label.value) {
|
||||
labels[label.name] = label.value;
|
||||
}
|
||||
});
|
||||
profile.Labels = labels;
|
||||
}
|
||||
|
||||
$scope.create = function () {
|
||||
var profile = $scope.model;
|
||||
|
||||
|
@ -30,6 +53,23 @@ function ($scope, $state, $transition$, Notifications, StoridgeProfileService) {
|
|||
delete profile.MaxBandwidth;
|
||||
}
|
||||
|
||||
if (profile.SnapshotEnabled) {
|
||||
if (!profile.SnapshotMax || profile.SnapshotMax <= 0) {
|
||||
profile.SnapshotMax = 1;
|
||||
}
|
||||
if (!$scope.state.RecurringSnapshotEnabled) {
|
||||
delete profile.SnapshotInterval;
|
||||
}
|
||||
if ($scope.state.RecurringSnapshotEnabled && (!profile.SnapshotInterval || profile.SnapshotInterval <= 0)) {
|
||||
profile.SnapshotInterval = 1440;
|
||||
}
|
||||
} else {
|
||||
delete profile.SnapshotMax;
|
||||
delete profile.SnapshotInterval;
|
||||
}
|
||||
|
||||
prepareLabels(profile);
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
StoridgeProfileService.create(profile)
|
||||
.then(function success() {
|
||||
|
@ -47,7 +87,7 @@ function ($scope, $state, $transition$, Notifications, StoridgeProfileService) {
|
|||
$scope.updatedName = function() {
|
||||
if (!$scope.state.ManualInputDirectory) {
|
||||
var profile = $scope.model;
|
||||
profile.Directory = '/cio/' + profile.Name;
|
||||
profile.Directory = '/cio/' + (profile.Name ? _.toLower(profile.Name) : '');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -60,7 +100,7 @@ function ($scope, $state, $transition$, Notifications, StoridgeProfileService) {
|
|||
function initView() {
|
||||
var profile = new StoridgeProfileDefaultModel();
|
||||
profile.Name = $transition$.params().profileName;
|
||||
profile.Directory = '/cio/' + profile.Name;
|
||||
profile.Directory = profile.Directory + _.toLower(profile.Name);
|
||||
$scope.model = profile;
|
||||
}
|
||||
|
||||
|
|
|
@ -91,20 +91,164 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !type -->
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group">
|
||||
<label for="profile_filesystem" class="col-sm-2 col-lg-1 control-label text-left">Filesystem</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select name="profile_filesystem" ng-model="model.Filesystem" class="form-control">
|
||||
<option value="btrfs">btrfs</option>
|
||||
<option value="ext4">ext4</option>
|
||||
<option value="xfs">xfs</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Filesystem -->
|
||||
<!-- snapshotEnabled -->
|
||||
<div class="form-group">
|
||||
<label for="profile_snapshotEnabled" class="col-sm-2 col-lg-1 control-label text-left">
|
||||
Enable snapshots
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input name="profile_snapshotEnabled" type="checkbox" ng-model="model.SnapshotEnabled"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
<!-- !snapshotEnabled -->
|
||||
<!-- snapshotMax -->
|
||||
<div class="form-group" ng-if="model.SnapshotEnabled">
|
||||
<label for="profile_snapshotMax" class="col-sm-2 col-lg-1 control-label text-left" style="margin-top: 20px;">
|
||||
Snapshot max
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<slider model="model.SnapshotMax" floor="1" ceil="100" step="1"></slider>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<input type="number" min="1" class="form-control" ng-model="model.SnapshotMax" id="profile_snapshotMax">
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p class="small text-muted" style="margin-top: 7px;">
|
||||
Snapshot max (<b>count</b>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !snapshotMax -->
|
||||
<!-- snapshotEnabled -->
|
||||
<div class="form-group" ng-if="model.SnapshotEnabled">
|
||||
<label for="profile_recurringSnapshotEnabled" class="col-sm-2 col-lg-1 control-label text-left">
|
||||
Enable periodic snapshots
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input name="profile_recurringSnapshotEnabled" type="checkbox" ng-model="state.RecurringSnapshotEnabled"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
<!-- !snapshotEnabled -->
|
||||
<!-- snapshotInterval -->
|
||||
<div class="form-group" ng-if="model.SnapshotEnabled && state.RecurringSnapshotEnabled">
|
||||
<label for="profile_snapshotInterval" class="col-sm-2 col-lg-1 control-label text-left" style="margin-top: 20px;">
|
||||
Snapshot interval
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<slider model="model.SnapshotInterval" floor="1" ceil="2880" step="1"></slider>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<input type="number" min="1" class="form-control" ng-model="model.SnapshotInterval" id="profile_snapshotInterval">
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p class="small text-muted" style="margin-top: 7px;">
|
||||
Snapshot interval (<b>minutes</b>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !snapshotInterval -->
|
||||
<!-- encryptionEnabled -->
|
||||
<div class="form-group">
|
||||
<label for="profile_encryptionEnabled" class="col-sm-2 col-lg-1 control-label text-left">
|
||||
Enable encryption
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input name="profile_encryptionEnabled" type="checkbox" ng-model="model.EncryptionEnabled"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
<!-- !encryptionEnabled -->
|
||||
<!-- interfaceType -->
|
||||
<div class="form-group">
|
||||
<label for="profile_interfaceType" class="col-sm-2 col-lg-1 control-label text-left">Interface type</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select name="profile_interfaceType" ng-model="model.InterfaceType" class="form-control">
|
||||
<option value=""></option>
|
||||
<option value="nfs">nfs</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !interfaceType -->
|
||||
<!-- interfaceDriver -->
|
||||
<div class="form-group">
|
||||
<label for="profile_interfaceDriver" class="col-sm-2 col-lg-1 control-label text-left">Network driver</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select name="profile_interfaceDriver" ng-model="model.InterfaceDriver" class="form-control">
|
||||
<option value=""></option>
|
||||
<option value="macvlan">macvlan</option>
|
||||
<option value="overlay">overlay</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !interfaceDriver -->
|
||||
<!-- interfaceNetwork -->
|
||||
<div class="form-group">
|
||||
<label for="profile_interfaceNetwork" class="col-sm-2 col-lg-1 control-label text-left">Network name</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="text" class="form-control" ng-model="model.InterfaceNetwork" name="profile_interfaceNetwork" pattern="[a-zA-Z0-9]+">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !interfaceNetwork -->
|
||||
<!-- interfaceConf -->
|
||||
<div class="form-group">
|
||||
<label for="profile_interfaceConf" class="col-sm-2 col-lg-1 control-label text-left">Interface conf</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="text" class="form-control" ng-model="model.InterfaceConf" name="profile_interfaceConf">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !interfaceConf -->
|
||||
<!-- labels -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Labels</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !labels -->
|
||||
<!-- 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">
|
||||
<div class="col-md-1 col-sm-2">
|
||||
<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>
|
||||
<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 class="form-group" ng-if="state.LimitIOPS">
|
||||
<label for="min_iops" class="col-sm-1 control-label text-left">Min</label>
|
||||
|
@ -142,14 +286,14 @@
|
|||
Bandwidth
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<div class="col-md-1 col-sm-2">
|
||||
<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>
|
||||
<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 class="form-group" ng-if="state.LimitBandwidth">
|
||||
<label for="min_bandwidth" class="col-sm-1 control-label text-left">Min</label>
|
||||
|
|
|
@ -84,6 +84,150 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !type -->
|
||||
<!-- Filesystem -->
|
||||
<div class="form-group">
|
||||
<label for="profile_filesystem" class="col-sm-2 col-lg-1 control-label text-left">Filesystem</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select name="profile_filesystem" ng-model="profile.Filesystem" class="form-control">
|
||||
<option value="btrfs">btrfs</option>
|
||||
<option value="ext4">ext4</option>
|
||||
<option value="xfs">xfs</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !Filesystem -->
|
||||
<!-- snapshotEnabled -->
|
||||
<div class="form-group">
|
||||
<label for="profile_snapshotEnabled" class="col-sm-2 col-lg-1 control-label text-left">
|
||||
Enable snapshots
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input name="profile_snapshotEnabled" type="checkbox" ng-model="profile.SnapshotEnabled"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
<!-- !snapshotEnabled -->
|
||||
<!-- snapshotMax -->
|
||||
<div class="form-group" ng-if="profile.SnapshotEnabled">
|
||||
<label for="profile_snapshotMax" class="col-sm-2 col-lg-1 control-label text-left" style="margin-top: 20px;">
|
||||
Snapshot max
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<slider model="profile.SnapshotMax" floor="1" ceil="100" step="1"></slider>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<input type="number" min="1" class="form-control" ng-model="profile.SnapshotMax" id="profile_snapshotMax">
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p class="small text-muted" style="margin-top: 7px;">
|
||||
Snapshot max (<b>count</b>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !snapshotMax -->
|
||||
<!-- snapshotEnabled -->
|
||||
<div class="form-group" ng-if="profile.SnapshotEnabled">
|
||||
<label for="profile_recurringSnapshotEnabled" class="col-sm-2 col-lg-1 control-label text-left">
|
||||
Enable periodic snapshots
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input name="profile_recurringSnapshotEnabled" type="checkbox" ng-model="state.RecurringSnapshotEnabled"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
<!-- !snapshotEnabled -->
|
||||
<!-- snapshotInterval -->
|
||||
<div class="form-group" ng-if="profile.SnapshotEnabled && state.RecurringSnapshotEnabled">
|
||||
<label for="profile_snapshotInterval" class="col-sm-2 col-lg-1 control-label text-left" style="margin-top: 20px;">
|
||||
Snapshot interval
|
||||
</label>
|
||||
<div class="col-sm-4">
|
||||
<slider model="profile.SnapshotInterval" floor="1" ceil="2880" step="1"></slider>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<input type="number" min="1" class="form-control" ng-model="profile.SnapshotInterval" id="profile_snapshotInterval">
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<p class="small text-muted" style="margin-top: 7px;">
|
||||
Snapshot interval (<b>minutes</b>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !snapshotInterval -->
|
||||
<!-- encryptionEnabled -->
|
||||
<div class="form-group">
|
||||
<label for="profile_encryptionEnabled" class="col-sm-2 col-lg-1 control-label text-left">
|
||||
Enable encryption
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input name="profile_encryptionEnabled" type="checkbox" ng-model="profile.EncryptionEnabled"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
<!-- !encryptionEnabled -->
|
||||
<!-- interfaceType -->
|
||||
<div class="form-group">
|
||||
<label for="profile_interfaceType" class="col-sm-2 col-lg-1 control-label text-left">Interface type</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select name="profile_interfaceType" ng-model="profile.InterfaceType" class="form-control">
|
||||
<option value=""></option>
|
||||
<option value="nfs">nfs</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !interfaceType -->
|
||||
<!-- interfaceDriver -->
|
||||
<div class="form-group">
|
||||
<label for="profile_interfaceDriver" class="col-sm-2 col-lg-1 control-label text-left">Network driver</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<select name="profile_interfaceDriver" ng-model="profile.InterfaceDriver" class="form-control">
|
||||
<option value=""></option>
|
||||
<option value="macvlan">macvlan</option>
|
||||
<option value="overlay">overlay</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !interfaceDriver -->
|
||||
<!-- interfaceNetwork -->
|
||||
<div class="form-group">
|
||||
<label for="profile_interfaceNetwork" class="col-sm-2 col-lg-1 control-label text-left">Network name</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="text" class="form-control" ng-model="profile.InterfaceNetwork" name="profile_interfaceNetwork" pattern="[a-zA-Z0-9]+">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !interfaceNetwork -->
|
||||
<!-- interfaceConf -->
|
||||
<div class="form-group">
|
||||
<label for="profile_interfaceConf" class="col-sm-2 col-lg-1 control-label text-left">Interface conf</label>
|
||||
<div class="col-sm-10 col-lg-11">
|
||||
<input type="text" class="form-control" ng-model="profile.InterfaceConf" name="profile_interfaceConf">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !interfaceConf -->
|
||||
<!-- labels -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Labels</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !labels -->
|
||||
<!-- iops -->
|
||||
<div ng-if="!state.LimitBandwidth || state.NoLimit">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
|
|
|
@ -2,21 +2,49 @@ angular.module('extension.storidge')
|
|||
.controller('StoridgeProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', 'ModalService',
|
||||
function ($scope, $state, $transition$, Notifications, StoridgeProfileService, ModalService) {
|
||||
|
||||
$scope.formValues = {
|
||||
Labels: []
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
NoLimit: false,
|
||||
LimitIOPS: false,
|
||||
LimitBandwidth: false,
|
||||
updateInProgress: false,
|
||||
deleteInProgress: false
|
||||
deleteInProgress: false,
|
||||
RecurringSnapshotEnabled: false
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.formValues.Labels.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.formValues.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
function prepareLabels(profile) {
|
||||
var labels = {};
|
||||
$scope.formValues.Labels.forEach(function (label) {
|
||||
if (label.name && label.value) {
|
||||
labels[label.name] = label.value;
|
||||
}
|
||||
});
|
||||
profile.Labels = labels;
|
||||
}
|
||||
|
||||
function initLabels(labels) {
|
||||
$scope.formValues.Labels = Object.keys(labels).map(function(key) {
|
||||
return { name:key, value:labels[key] };
|
||||
});
|
||||
}
|
||||
|
||||
$scope.RedundancyOptions = [
|
||||
{ value: 2, label: '2-copy' },
|
||||
{ value: 3, label: '3-copy' }
|
||||
];
|
||||
|
||||
$scope.update = function() {
|
||||
|
||||
var profile = $scope.profile;
|
||||
|
||||
if (!$scope.state.LimitIOPS) {
|
||||
|
@ -29,6 +57,23 @@ function ($scope, $state, $transition$, Notifications, StoridgeProfileService, M
|
|||
delete profile.MaxBandwidth;
|
||||
}
|
||||
|
||||
if (profile.SnapshotEnabled) {
|
||||
if (!profile.SnapshotMax || profile.SnapshotMax <= 0) {
|
||||
profile.SnapshotMax = 1;
|
||||
}
|
||||
if (!$scope.state.RecurringSnapshotEnabled) {
|
||||
delete profile.SnapshotInterval;
|
||||
}
|
||||
if ($scope.state.RecurringSnapshotEnabled && (!profile.SnapshotInterval || profile.SnapshotInterval <= 0)) {
|
||||
profile.SnapshotInterval = 1440;
|
||||
}
|
||||
} else {
|
||||
delete profile.SnapshotMax;
|
||||
delete profile.SnapshotInterval;
|
||||
}
|
||||
|
||||
prepareLabels(profile);
|
||||
|
||||
$scope.state.updateInProgress = true;
|
||||
StoridgeProfileService.update(profile)
|
||||
.then(function success() {
|
||||
|
@ -81,6 +126,10 @@ function ($scope, $state, $transition$, Notifications, StoridgeProfileService, M
|
|||
} else {
|
||||
$scope.state.NoLimit = true;
|
||||
}
|
||||
if (profile.SnapshotEnabled && profile.SnapshotInterval !== 0) {
|
||||
$scope.state.RecurringSnapshotEnabled = true;
|
||||
}
|
||||
initLabels(profile.Labels);
|
||||
$scope.profile = profile;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import _ from 'lodash-es';
|
||||
import { StoridgeProfileDefaultModel } from '../../models/profile';
|
||||
|
||||
angular.module('extension.storidge')
|
||||
|
@ -35,8 +36,13 @@ function ($q, $scope, $state, Notifications, StoridgeProfileService) {
|
|||
|
||||
$scope.create = function() {
|
||||
var model = new StoridgeProfileDefaultModel();
|
||||
model.Labels = {};
|
||||
model.Name = $scope.formValues.Name;
|
||||
model.Directory = model.Directory + model.Name;
|
||||
model.Directory = model.Directory + _.toLower(model.Name);
|
||||
delete model.MinBandwidth;
|
||||
delete model.MaxBandwidth;
|
||||
delete model.MinIOPS;
|
||||
delete model.MaxIOPS;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
StoridgeProfileService.create(model)
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Snasphot details"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="docker.volumes">Volumes</a> > <a ui-sref="docker.volumes.volume({id: volumeId})">{{ volumeId }}</a> > Snapshots > <a ui-sref="docker.volumes.volume.snapshot({id: snapshot.Id})">{{ snapshot.Id }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-hdd" title-text="Snapshot details "></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>
|
||||
{{ snapshot.Id }}
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeSnapshot()">
|
||||
<i class="fa fa-trash space-right" aria-hidden="true"></i>Remove snapshot
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>{{ snapshot.Date }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ snapshot.Description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SourceID</td>
|
||||
<td>{{ snapshot.SourceID }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ snapshot.Type }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Directory</td>
|
||||
<td>{{ snapshot.Directory }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Source</td>
|
||||
<td>{{ snapshot.Source }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,44 @@
|
|||
angular.module('extension.storidge')
|
||||
.controller('StoridgeSnapshotController', ['$scope', '$state', '$transition$', 'Notifications', 'ModalService', 'StoridgeSnapshotService',
|
||||
function ($scope, $state, $transition$, Notifications, ModalService, StoridgeSnapshotService) {
|
||||
|
||||
$scope.removeSnapshot = function () {
|
||||
ModalService.confirm({
|
||||
title: 'Are you sure?',
|
||||
message: 'Do you want really want to remove this snapshot?',
|
||||
buttons: {
|
||||
confirm: {
|
||||
label: 'Remove',
|
||||
className: 'btn-danger'
|
||||
}
|
||||
},
|
||||
callback: function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
StoridgeSnapshotService.remove($scope.snapshot.Id)
|
||||
.then(function () {
|
||||
Notifications.success('Success', 'Snapshot removed');
|
||||
$state.go('portainer.volumes.volume', {id: $scope.volumeId});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove snapshot');
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
$scope.volumeId = $transition$.params().id;
|
||||
$scope.snapshotId = $transition$.params().snapshotId;
|
||||
|
||||
StoridgeSnapshotService.snapshot($scope.snapshotId)
|
||||
.then(function success(data) {
|
||||
$scope.snapshot = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve snapshot details');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
}]);
|
|
@ -1,13 +1,13 @@
|
|||
import './datatable.css';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.controller('GenericDatatableController', ['PaginationService', 'DatatableService',
|
||||
function (PaginationService, DatatableService) {
|
||||
.controller('GenericDatatableController', ['PaginationService', 'DatatableService', 'PAGINATION_MAX_ITEMS',
|
||||
function (PaginationService, DatatableService, PAGINATION_MAX_ITEMS) {
|
||||
|
||||
this.state = {
|
||||
selectAll: false,
|
||||
orderBy: this.orderBy,
|
||||
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
|
||||
paginatedItemLimit: PAGINATION_MAX_ITEMS,
|
||||
displayTextFilter: false,
|
||||
selectedItemCount: 0,
|
||||
selectedItems: []
|
||||
|
@ -67,5 +67,6 @@ function (PaginationService, DatatableService) {
|
|||
function setDefaults(ctrl) {
|
||||
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
|
||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
||||
ctrl.state.paginatedItemLimit = PaginationService.getPaginationLimit(ctrl.tableKey);
|
||||
}
|
||||
}]);
|
||||
|
|
|
@ -10,7 +10,7 @@ angular.module('portainer.app')
|
|||
showSelectionBar: true,
|
||||
enforceStep: false,
|
||||
translate: function(value, sliderId, label) {
|
||||
if (label === 'floor' || value === 0) {
|
||||
if ((label === 'floor' && ctrl.floor === 0) || value === 0) {
|
||||
return 'unlimited';
|
||||
}
|
||||
return value;
|
||||
|
|
|
@ -38,7 +38,7 @@ angular.module('portainer.app')
|
|||
.filter('capitalize', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
return _.capitalize(text);
|
||||
return text ? _.capitalize(text) : '';
|
||||
};
|
||||
})
|
||||
.filter('stripprotocol', function() {
|
||||
|
|
|
@ -2,8 +2,8 @@ import _ from 'lodash-es';
|
|||
|
||||
// TODO: legacy extension management
|
||||
angular.module('portainer.app')
|
||||
.factory('LegacyExtensionManager', ['$q', 'PluginService', 'SystemService', 'LegacyExtensionService',
|
||||
function ExtensionManagerFactory($q, PluginService, SystemService, LegacyExtensionService) {
|
||||
.factory('LegacyExtensionManager', ['$q', 'PluginService', 'SystemService', 'NodeService', 'LegacyExtensionService',
|
||||
function ExtensionManagerFactory($q, PluginService, SystemService, NodeService, LegacyExtensionService) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
|
@ -59,9 +59,9 @@ function ExtensionManagerFactory($q, PluginService, SystemService, LegacyExtensi
|
|||
function registerStoridgeUsingSwarmManagerIP() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
SystemService.info()
|
||||
NodeService.getActiveManager()
|
||||
.then(function success(data) {
|
||||
var managerIP = data.Swarm.NodeAddr;
|
||||
var managerIP = data.Addr;
|
||||
var storidgeAPIURL = 'tcp://' + managerIP + ':8282';
|
||||
return LegacyExtensionService.registerStoridgeExtension(storidgeAPIURL);
|
||||
})
|
||||
|
|
|
@ -60,10 +60,10 @@ angular.module('portainer.app')
|
|||
localStorageService.remove('JWT');
|
||||
},
|
||||
storePaginationLimit: function(key, count) {
|
||||
localStorageService.cookie.set('pagination_' + key, count);
|
||||
localStorageService.set('datatable_pagination_' + key, count);
|
||||
},
|
||||
getPaginationLimit: function(key) {
|
||||
return localStorageService.cookie.get('pagination_' + key);
|
||||
return localStorageService.get('datatable_pagination_' + key);
|
||||
},
|
||||
getDataTableOrder: function(key) {
|
||||
return localStorageService.get('datatable_order_' + key);
|
||||
|
|
|
@ -21,6 +21,8 @@ angular.module('portainer.app')
|
|||
msg = e.data.details;
|
||||
} else if (e.data && e.data.message) {
|
||||
msg = e.data.message;
|
||||
} else if (e.data && e.data.content) {
|
||||
msg = e.data.content;
|
||||
} else if (e.message) {
|
||||
msg = e.message;
|
||||
} else if (e.err && e.err.data && e.err.data.message) {
|
||||
|
|
|
@ -25,16 +25,19 @@
|
|||
offline-mode="endpointState.OfflineMode"
|
||||
></docker-sidebar-content>
|
||||
<li class="sidebar-title" ng-if="applicationState.endpoint.mode && applicationState.endpoint.extensions.length > 0">
|
||||
<span>Extensions</span>
|
||||
<span>Integrations</span>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode && 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.new' || $state.current.name === 'storidge.profiles.profile')">
|
||||
<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.new' || $state.current.name === 'storidge.profiles.profile' || $state.current.name === 'storidge.drives' || $state.current.name === 'storidge.drives.drive')">
|
||||
<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.new' || $state.current.name === 'storidge.profiles.profile')">
|
||||
<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.new' || $state.current.name === 'storidge.profiles.profile' || $state.current.name === 'storidge.drives' || $state.current.name === 'storidge.drives.drive')">
|
||||
<a ui-sref="storidge.profiles" ui-sref-active="active">Profiles</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.new' || $state.current.name === 'storidge.profiles.profile' || $state.current.name === 'storidge.drives' || $state.current.name === 'storidge.drives.drive')">
|
||||
<a ui-sref="storidge.drives" ui-sref-active="active">Drives</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-title" ng-if="(!applicationState.application.authentication || isAdmin) && applicationState.application.enableHostManagementFeatures">
|
||||
<span>Scheduler</span>
|
||||
|
|
|
@ -144,6 +144,10 @@ a[ng-click]{
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.widget .widget-body table.description-table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.template-widget {
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -267,6 +271,12 @@ a[ng-click]{
|
|||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.btn-datatable {
|
||||
padding: 2.6px 7.8px 3.9px;
|
||||
line-height: 1;
|
||||
margin: 0px 0px 0px 0px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1107px) {
|
||||
.btn-responsive {
|
||||
padding: 6px 12px;
|
||||
|
|
Loading…
Reference in New Issue