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 @@ +
+ + +
+
{{ $ctrl.titleText }}
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Storage + + + + + + Usage + + + +
+ + + + {{ item.Name }}{{ item.Size }}
+ + {{ vol.PersistentVolumeClaim.Name }} + + + {{ vol.PersistentVolumeClaim.Storage }} +
Loading...
No storage available.
+
+ +
+
+
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;