diff --git a/api/kubernetes/cli/role.go b/api/kubernetes/cli/role.go
index e19f1f22a..d3a233202 100644
--- a/api/kubernetes/cli/role.go
+++ b/api/kubernetes/cli/role.go
@@ -13,6 +13,11 @@ func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
Resources: []string{"namespaces", "nodes"},
APIGroups: []string{""},
},
+ {
+ Verbs: []string{"list"},
+ Resources: []string{"storageclasses"},
+ APIGroups: []string{"storage.k8s.io"},
+ },
}
}
diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html
index e30c5c73b..f1efdf899 100644
--- a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html
+++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html
@@ -168,10 +168,10 @@
{{ item.CreationDate | getisodate }} {{ item.ApplicationOwner ? 'by ' + item.ApplicationOwner : '' }} |
- Loading... |
+ Loading... |
- No application available. |
+ No application available. |
diff --git a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html
index 7955c6ed2..e37c7b9e6 100644
--- a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html
+++ b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html
@@ -159,10 +159,10 @@
- Loading... |
+ Loading... |
- No volume available. |
+ No volume available. |
diff --git a/app/kubernetes/converters/persistentVolumeClaim.js b/app/kubernetes/converters/persistentVolumeClaim.js
index 29838a86c..8a7a76060 100644
--- a/app/kubernetes/converters/persistentVolumeClaim.js
+++ b/app/kubernetes/converters/persistentVolumeClaim.js
@@ -12,7 +12,7 @@ class KubernetesPersistentVolumeClaimConverter {
res.Name = data.metadata.name;
res.Namespace = data.metadata.namespace;
res.CreationDate = data.metadata.creationTimestamp;
- res.Storage = data.spec.resources.requests.storage.replace('i', '') + 'B';
+ res.Storage = data.spec.resources.requests.storage.replace('i', 'B');
res.StorageClass = _.find(storageClasses, { Name: data.spec.storageClassName });
res.Yaml = yaml ? yaml.data : '';
res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] : '';
diff --git a/app/kubernetes/views/applications/applications.js b/app/kubernetes/views/applications/applications.js
index e4b0de22c..7994db0eb 100644
--- a/app/kubernetes/views/applications/applications.js
+++ b/app/kubernetes/views/applications/applications.js
@@ -2,4 +2,7 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsView', {
templateUrl: './applications.html',
controller: 'KubernetesApplicationsController',
controllerAs: 'ctrl',
+ bindings: {
+ $transition$: '<',
+ },
});
diff --git a/app/kubernetes/views/applications/applicationsController.js b/app/kubernetes/views/applications/applicationsController.js
index 07553810e..a0f538590 100644
--- a/app/kubernetes/views/applications/applicationsController.js
+++ b/app/kubernetes/views/applications/applicationsController.js
@@ -1,5 +1,5 @@
import angular from 'angular';
-import _ from 'lodash-es';
+import * as _ from 'lodash-es';
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
@@ -10,6 +10,7 @@ class KubernetesApplicationsController {
this.$state = $state;
this.Notifications = Notifications;
this.KubernetesApplicationService = KubernetesApplicationService;
+
this.Authentication = Authentication;
this.ModalService = ModalService;
this.LocalStorage = LocalStorage;
@@ -95,9 +96,10 @@ class KubernetesApplicationsController {
async getApplicationsAsync() {
try {
- this.applications = await this.KubernetesApplicationService.get();
- this.stacks = KubernetesStackHelper.stacksFromApplications(this.applications);
- this.ports = KubernetesApplicationHelper.portMappingsFromApplications(this.applications);
+ const applications = await this.KubernetesApplicationService.get();
+ this.applications = applications;
+ this.stacks = KubernetesStackHelper.stacksFromApplications(applications);
+ this.ports = KubernetesApplicationHelper.portMappingsFromApplications(applications);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
}
@@ -109,14 +111,12 @@ class KubernetesApplicationsController {
async onInit() {
this.state = {
- activeTab: 0,
+ activeTab: this.LocalStorage.getActiveTab('applications'),
currentName: this.$state.$current.name,
isAdmin: this.Authentication.isAdmin(),
viewReady: false,
};
- this.state.activeTab = this.LocalStorage.getActiveTab('applications');
-
await this.getApplications();
this.state.viewReady = true;
diff --git a/app/kubernetes/views/volumes/components/volumes-storages-datatable/controller.js b/app/kubernetes/views/volumes/components/volumes-storages-datatable/controller.js
new file mode 100644
index 000000000..7b5339570
--- /dev/null
+++ b/app/kubernetes/views/volumes/components/volumes-storages-datatable/controller.js
@@ -0,0 +1,79 @@
+import _ from 'lodash-es';
+
+angular.module('portainer.kubernetes').controller('KubernetesVolumesStoragesDatatableController', [
+ '$scope',
+ '$controller',
+ 'DatatableService',
+ function ($scope, $controller, DatatableService) {
+ angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
+ this.state = Object.assign(this.state, {
+ expandedItems: [],
+ expandAll: false,
+ });
+
+ this.onSettingsRepeaterChange = function () {
+ DatatableService.setDataTableSettings(this.tableKey, this.settings);
+ };
+
+ this.expandItem = function (item, expanded) {
+ if (!this.itemCanExpand(item)) {
+ return;
+ }
+
+ item.Expanded = expanded;
+ if (!expanded) {
+ item.Highlighted = false;
+ }
+ };
+
+ this.itemCanExpand = function (item) {
+ return item.Volumes.length > 0;
+ };
+
+ this.hasExpandableItems = function () {
+ return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length;
+ };
+
+ this.expandAll = function () {
+ this.state.expandAll = !this.state.expandAll;
+ _.forEach(this.state.filteredDataSet, (item) => {
+ if (this.itemCanExpand(item)) {
+ this.expandItem(item, this.state.expandAll);
+ }
+ });
+ };
+
+ this.$onInit = function () {
+ this.setDefaults();
+ this.prepareTableFromDataset();
+
+ this.state.orderBy = this.orderBy;
+ var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
+ if (storedOrder !== null) {
+ this.state.reverseOrder = storedOrder.reverse;
+ this.state.orderBy = storedOrder.orderBy;
+ }
+
+ var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
+ if (textFilter !== null) {
+ this.state.textFilter = textFilter;
+ this.onTextFilterChange();
+ }
+
+ var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
+ if (storedFilters !== null) {
+ this.filters = storedFilters;
+ }
+ if (this.filters && this.filters.state) {
+ this.filters.state.open = false;
+ }
+
+ var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
+ if (storedSettings !== null) {
+ this.settings = storedSettings;
+ this.settings.open = false;
+ }
+ this.onSettingsRepeaterChange();
+ };
+ },
+]);
diff --git a/app/kubernetes/views/volumes/components/volumes-storages-datatable/index.js b/app/kubernetes/views/volumes/components/volumes-storages-datatable/index.js
new file mode 100644
index 000000000..8a48e18c2
--- /dev/null
+++ b/app/kubernetes/views/volumes/components/volumes-storages-datatable/index.js
@@ -0,0 +1,13 @@
+angular.module('portainer.kubernetes').component('kubernetesVolumesStoragesDatatable', {
+ templateUrl: './template.html',
+ controller: 'KubernetesVolumesStoragesDatatableController',
+ bindings: {
+ titleText: '@',
+ titleIcon: '@',
+ dataset: '<',
+ tableKey: '@',
+ orderBy: '@',
+ reverseOrder: '<',
+ refreshCallback: '<',
+ },
+});
diff --git a/app/kubernetes/views/volumes/components/volumes-storages-datatable/template.html b/app/kubernetes/views/volumes/components/volumes-storages-datatable/template.html
new file mode 100644
index 000000000..5d1f090c4
--- /dev/null
+++ b/app/kubernetes/views/volumes/components/volumes-storages-datatable/template.html
@@ -0,0 +1,139 @@
+
diff --git a/app/kubernetes/views/volumes/volumes.html b/app/kubernetes/views/volumes/volumes.html
index 4c9ba2f81..71a328591 100644
--- a/app/kubernetes/views/volumes/volumes.html
+++ b/app/kubernetes/views/volumes/volumes.html
@@ -7,14 +7,28 @@
-
-
+
+
+
+
+ Volumes
+
+
+
+
+ Storage
+
+
+
+
+
+
diff --git a/app/kubernetes/views/volumes/volumes.js b/app/kubernetes/views/volumes/volumes.js
index 98829ddc1..f004c20ff 100644
--- a/app/kubernetes/views/volumes/volumes.js
+++ b/app/kubernetes/views/volumes/volumes.js
@@ -2,4 +2,7 @@ angular.module('portainer.kubernetes').component('kubernetesVolumesView', {
templateUrl: './volumes.html',
controller: 'KubernetesVolumesController',
controllerAs: 'ctrl',
+ bindings: {
+ $transition$: '<',
+ },
});
diff --git a/app/kubernetes/views/volumes/volumesController.js b/app/kubernetes/views/volumes/volumesController.js
index 2fcb899a3..90d44cf9f 100644
--- a/app/kubernetes/views/volumes/volumesController.js
+++ b/app/kubernetes/views/volumes/volumesController.js
@@ -1,14 +1,52 @@
-import _ from 'lodash-es';
+import * as _ from 'lodash-es';
+import filesizeParser from 'filesize-parser';
import angular from 'angular';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
+function buildStorages(storages, volumes) {
+ _.forEach(storages, (s) => {
+ const filteredVolumes = _.filter(volumes, ['PersistentVolumeClaim.StorageClass.Name', s.Name, 'PersistentVolumeClaim.StorageClass.Provisioner', s.Provisioner]);
+ s.Volumes = filteredVolumes;
+ s.Size = computeSize(filteredVolumes);
+ });
+ return storages;
+}
+
+function computeSize(volumes) {
+ let hasT,
+ hasG,
+ hasM = false;
+ const size = _.sumBy(volumes, (v) => {
+ const storage = v.PersistentVolumeClaim.Storage;
+ if (!hasT && _.endsWith(storage, 'TB')) {
+ hasT = true;
+ } else if (!hasG && _.endsWith(storage, 'GB')) {
+ hasG = true;
+ } else if (!hasM && _.endsWith(storage, 'MB')) {
+ hasM = true;
+ }
+ return filesizeParser(storage, { base: 10 });
+ });
+ if (hasT) {
+ return size / 1000 / 1000 / 1000 / 1000 + 'TB';
+ } else if (hasG) {
+ return size / 1000 / 1000 / 1000 + 'GB';
+ } else if (hasM) {
+ return size / 1000 / 1000 + 'MB';
+ }
+ return size;
+}
+
class KubernetesVolumesController {
/* @ngInject */
- constructor($async, $state, Notifications, ModalService, KubernetesVolumeService, KubernetesApplicationService) {
+ constructor($async, $state, Notifications, ModalService, LocalStorage, EndpointProvider, KubernetesStorageService, KubernetesVolumeService, KubernetesApplicationService) {
this.$async = $async;
this.$state = $state;
this.Notifications = Notifications;
this.ModalService = ModalService;
+ this.LocalStorage = LocalStorage;
+ this.EndpointProvider = EndpointProvider;
+ this.KubernetesStorageService = KubernetesStorageService;
this.KubernetesVolumeService = KubernetesVolumeService;
this.KubernetesApplicationService = KubernetesApplicationService;
@@ -19,6 +57,10 @@ class KubernetesVolumesController {
this.removeActionAsync = this.removeActionAsync.bind(this);
}
+ selectTab(index) {
+ this.LocalStorage.storeActiveTab('volumes', index);
+ }
+
async removeActionAsync(selectedItems) {
let actionCount = selectedItems.length;
for (const volume of selectedItems) {
@@ -48,12 +90,17 @@ class KubernetesVolumesController {
async getVolumesAsync() {
try {
- const [volumes, applications] = await Promise.all([this.KubernetesVolumeService.get(), this.KubernetesApplicationService.get()]);
+ const [volumes, applications, storages] = await Promise.all([
+ this.KubernetesVolumeService.get(),
+ this.KubernetesApplicationService.get(),
+ this.KubernetesStorageService.get(this.state.endpointId),
+ ]);
this.volumes = _.map(volumes, (volume) => {
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
return volume;
});
+ this.storages = buildStorages(storages, volumes);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retreive resource pools');
}
@@ -66,6 +113,10 @@ class KubernetesVolumesController {
async onInit() {
this.state = {
viewReady: false,
+ // endpointId: this.$transition$.params().endpointId, // TODO: use this when moving to endpointID in URL
+ currentName: this.$state.$current.name,
+ endpointId: this.EndpointProvider.endpointID(),
+ activeTab: this.LocalStorage.getActiveTab('volumes'),
};
await this.getVolumes();
@@ -76,6 +127,12 @@ class KubernetesVolumesController {
$onInit() {
return this.$async(this.onInit);
}
+
+ $onDestroy() {
+ if (this.state.currentName !== this.$state.$current.name) {
+ this.LocalStorage.storeActiveTab('volumes', 0);
+ }
+ }
}
export default KubernetesVolumesController;