mirror of
https://github.com/portainer/portainer.git
synced 2026-08-07 10:14:48 +00:00
fix(kubernetes): improve PVC deletion UX based on workload usage [R8S-1046] (#2766)
This commit is contained in:
@@ -44,6 +44,11 @@ func (handler *Handler) getAllKubernetesPersistentVolumeClaims(w http.ResponseWr
|
||||
return httperror.InternalServerError("failed to retrieve persistent volume claims", err)
|
||||
}
|
||||
|
||||
pvcs, err = cli.CombineClaimsWithApplications(pvcs)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("context", "GetAllKubernetesPersistentVolumeClaims").Msg("Failed to enrich PVCs with owning applications")
|
||||
}
|
||||
|
||||
return response.JSON(w, pvcs)
|
||||
}
|
||||
|
||||
@@ -83,6 +88,11 @@ func (handler *Handler) getKubernetesPVCsInNamespace(w http.ResponseWriter, r *h
|
||||
return httperror.InternalServerError("failed to retrieve persistent volume claims", err)
|
||||
}
|
||||
|
||||
pvcs, err = cli.CombineClaimsWithApplications(pvcs)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("context", "GetKubernetesPVCsInNamespace").Str("namespace", namespace).Msg("Failed to enrich PVCs with owning applications")
|
||||
}
|
||||
|
||||
return response.JSON(w, pvcs)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/rs/zerolog/log"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
@@ -167,3 +169,87 @@ func parsePersistentVolumeClaimDetail(pvc *corev1.PersistentVolumeClaim) models.
|
||||
Labels: pvc.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
// CombineClaimsWithApplications enriches each PVC with the workloads that mount it.
|
||||
func (kcl *KubeClient) CombineClaimsWithApplications(pvcs []models.K8sPersistentVolumeClaim) ([]models.K8sPersistentVolumeClaim, error) {
|
||||
pods, err := kcl.cli.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
return pvcs, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to list pods: %w", err)
|
||||
}
|
||||
|
||||
replicaSetItems := make([]appsv1.ReplicaSet, 0)
|
||||
deploymentItems := make([]appsv1.Deployment, 0)
|
||||
if containsReplicaSetOwnerReference(pods) {
|
||||
replicaSets, err := kcl.cli.AppsV1().ReplicaSets("").List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list replica sets: %w", err)
|
||||
}
|
||||
replicaSetItems = replicaSets.Items
|
||||
|
||||
deployments, err := kcl.cli.AppsV1().Deployments("").List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list deployments: %w", err)
|
||||
}
|
||||
deploymentItems = deployments.Items
|
||||
}
|
||||
|
||||
statefulSetItems := make([]appsv1.StatefulSet, 0)
|
||||
if containsStatefulSetOwnerReference(pods) {
|
||||
statefulSets, err := kcl.cli.AppsV1().StatefulSets("").List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list stateful sets: %w", err)
|
||||
}
|
||||
statefulSetItems = statefulSets.Items
|
||||
}
|
||||
|
||||
daemonSetItems := make([]appsv1.DaemonSet, 0)
|
||||
if containsDaemonSetOwnerReference(pods) {
|
||||
daemonSets, err := kcl.cli.AppsV1().DaemonSets("").List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list daemon sets: %w", err)
|
||||
}
|
||||
daemonSetItems = daemonSets.Items
|
||||
}
|
||||
|
||||
resources := PortainerApplicationResources{
|
||||
ReplicaSets: replicaSetItems,
|
||||
Deployments: deploymentItems,
|
||||
StatefulSets: statefulSetItems,
|
||||
DaemonSets: daemonSetItems,
|
||||
}
|
||||
|
||||
for i := range pvcs {
|
||||
for _, pod := range pods.Items {
|
||||
for _, podVolume := range pod.Spec.Volumes {
|
||||
if podVolume.PersistentVolumeClaim == nil {
|
||||
continue
|
||||
}
|
||||
if podVolume.PersistentVolumeClaim.ClaimName != pvcs[i].Name || pod.Namespace != pvcs[i].Namespace {
|
||||
continue
|
||||
}
|
||||
application, err := kcl.ConvertPodToApplication(pod, resources, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert pod to application: %w", err)
|
||||
}
|
||||
if application == nil {
|
||||
continue
|
||||
}
|
||||
alreadyAdded := false
|
||||
for _, existing := range pvcs[i].OwningApplications {
|
||||
if existing.Name == application.Name && existing.Namespace == application.Namespace {
|
||||
alreadyAdded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !alreadyAdded {
|
||||
pvcs[i].OwningApplications = append(pvcs[i].OwningApplications, *application)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pvcs, nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
storagev1 "k8s.io/api/storage/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
@@ -154,3 +155,224 @@ func TestGetPersistentVolumeClaims_EnrichesExpansionFlag(t *testing.T) {
|
||||
assert.False(t, pvcs[0].AllowVolumeExpansion)
|
||||
})
|
||||
}
|
||||
|
||||
func makePod(namespace, name, pvcName string) *corev1.Pod {
|
||||
return &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "data",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
|
||||
ClaimName: pvcName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Containers: []corev1.Container{{Name: "app", Image: "nginx"}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func makePodWithDeploymentOwner(namespace, name, pvcName, rsName string) *corev1.Pod {
|
||||
pod := makePod(namespace, name, pvcName)
|
||||
pod.OwnerReferences = []metav1.OwnerReference{
|
||||
{Kind: "ReplicaSet", Name: rsName, APIVersion: "apps/v1"},
|
||||
}
|
||||
return pod
|
||||
}
|
||||
|
||||
func makeReplicaSet(namespace, name, deploymentName string) *appsv1.ReplicaSet {
|
||||
return &appsv1.ReplicaSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{Kind: "Deployment", Name: deploymentName, APIVersion: "apps/v1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func makeDeployment(namespace, name string) *appsv1.Deployment {
|
||||
return &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Selector: &metav1.LabelSelector{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombineClaimsWithApplications(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("no pods leaves owningApplications empty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
k := NewTestKubeClient(kfake.NewClientset())
|
||||
|
||||
pvc := makePVC("default", "my-pvc", "")
|
||||
_, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pvcs, err := k.GetPersistentVolumeClaims("default")
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := k.CombineClaimsWithApplications(pvcs)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
assert.Empty(t, result[0].OwningApplications)
|
||||
})
|
||||
|
||||
t.Run("pod with no PVC volumes leaves owningApplications empty", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
k := NewTestKubeClient(kfake.NewClientset())
|
||||
|
||||
pvc := makePVC("default", "my-pvc", "")
|
||||
_, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "app", Namespace: "default"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "app", Image: "nginx"}},
|
||||
},
|
||||
}
|
||||
_, err = k.cli.CoreV1().Pods("default").Create(t.Context(), pod, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pvcs, err := k.GetPersistentVolumeClaims("default")
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := k.CombineClaimsWithApplications(pvcs)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result[0].OwningApplications)
|
||||
})
|
||||
|
||||
t.Run("pod mounting PVC populates owningApplications", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
k := NewTestKubeClient(kfake.NewClientset())
|
||||
|
||||
pvc := makePVC("default", "my-pvc", "")
|
||||
_, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pod := makePod("default", "my-pod", "my-pvc")
|
||||
_, err = k.cli.CoreV1().Pods("default").Create(t.Context(), pod, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pvcs, err := k.GetPersistentVolumeClaims("default")
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := k.CombineClaimsWithApplications(pvcs)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result[0].OwningApplications, 1)
|
||||
assert.Equal(t, "my-pod", result[0].OwningApplications[0].Name)
|
||||
assert.Equal(t, "Pod", result[0].OwningApplications[0].ApplicationType)
|
||||
assert.Equal(t, "default", result[0].OwningApplications[0].ResourcePool)
|
||||
})
|
||||
|
||||
t.Run("pod in different namespace does not match PVC", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
k := NewTestKubeClient(kfake.NewClientset())
|
||||
|
||||
pvc := makePVC("default", "my-pvc", "")
|
||||
_, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pod := makePod("other-ns", "other-pod", "my-pvc")
|
||||
_, err = k.cli.CoreV1().Pods("other-ns").Create(t.Context(), pod, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pvcs, err := k.GetPersistentVolumeClaims("default")
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := k.CombineClaimsWithApplications(pvcs)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result[0].OwningApplications)
|
||||
})
|
||||
|
||||
t.Run("two pods mounting the same PVC from the same Deployment are deduplicated", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
k := NewTestKubeClient(kfake.NewClientset())
|
||||
|
||||
pvc := makePVC("default", "shared-pvc", "")
|
||||
_, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
deploy := makeDeployment("default", "my-deploy")
|
||||
_, err = k.cli.AppsV1().Deployments("default").Create(t.Context(), deploy, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
rs := makeReplicaSet("default", "my-deploy-rs", "my-deploy")
|
||||
_, err = k.cli.AppsV1().ReplicaSets("default").Create(t.Context(), rs, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pod1 := makePodWithDeploymentOwner("default", "my-deploy-pod-1", "shared-pvc", "my-deploy-rs")
|
||||
pod2 := makePodWithDeploymentOwner("default", "my-deploy-pod-2", "shared-pvc", "my-deploy-rs")
|
||||
_, err = k.cli.CoreV1().Pods("default").Create(t.Context(), pod1, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
_, err = k.cli.CoreV1().Pods("default").Create(t.Context(), pod2, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pvcs, err := k.GetPersistentVolumeClaims("default")
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := k.CombineClaimsWithApplications(pvcs)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result[0].OwningApplications, 1)
|
||||
assert.Equal(t, "my-deploy", result[0].OwningApplications[0].Name)
|
||||
assert.Equal(t, "Deployment", result[0].OwningApplications[0].ApplicationType)
|
||||
})
|
||||
|
||||
t.Run("pod owned by Deployment resolves application name to Deployment", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
k := NewTestKubeClient(kfake.NewClientset())
|
||||
|
||||
pvc := makePVC("default", "app-pvc", "")
|
||||
_, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
deploy := makeDeployment("default", "web-app")
|
||||
_, err = k.cli.AppsV1().Deployments("default").Create(t.Context(), deploy, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
rs := makeReplicaSet("default", "web-app-rs-abc", "web-app")
|
||||
_, err = k.cli.AppsV1().ReplicaSets("default").Create(t.Context(), rs, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pod := makePodWithDeploymentOwner("default", "web-app-pod-xyz", "app-pvc", "web-app-rs-abc")
|
||||
_, err = k.cli.CoreV1().Pods("default").Create(t.Context(), pod, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pvcs, err := k.GetPersistentVolumeClaims("default")
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := k.CombineClaimsWithApplications(pvcs)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result[0].OwningApplications, 1)
|
||||
assert.Equal(t, "web-app", result[0].OwningApplications[0].Name)
|
||||
assert.Equal(t, "Deployment", result[0].OwningApplications[0].ApplicationType)
|
||||
assert.Equal(t, "default", result[0].OwningApplications[0].ResourcePool)
|
||||
})
|
||||
|
||||
t.Run("unrelated pod mounting a different PVC does not affect result", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
k := NewTestKubeClient(kfake.NewClientset())
|
||||
|
||||
pvc := makePVC("default", "target-pvc", "")
|
||||
_, err := k.cli.CoreV1().PersistentVolumeClaims("default").Create(t.Context(), pvc, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pod := makePod("default", "unrelated-pod", "other-pvc")
|
||||
_, err = k.cli.CoreV1().Pods("default").Create(t.Context(), pod, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
pvcs, err := k.GetPersistentVolumeClaims("default")
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := k.CombineClaimsWithApplications(pvcs)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, result[0].OwningApplications)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -90,6 +90,28 @@ const mockPVCs: PersistentVolumeClaim[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const pvcWithWorkload: PersistentVolumeClaim = {
|
||||
id: 'pvc-3',
|
||||
name: 'used-pvc',
|
||||
namespace: 'default',
|
||||
storage: 1073741824,
|
||||
storageRequest: '1Gi',
|
||||
creationDate: '2024-01-03T00:00:00Z',
|
||||
accessModes: ['ReadWriteOnce'],
|
||||
humanReadableAccessModes: ['ReadWriteOnce'],
|
||||
volumeName: 'pv-3',
|
||||
storageClass: 'standard',
|
||||
allowVolumeExpansion: false,
|
||||
phase: 'Bound',
|
||||
owningApplications: [
|
||||
{
|
||||
Name: 'my-deployment',
|
||||
ResourcePool: 'default',
|
||||
ApplicationType: 'Deployment',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockNamespaces = [
|
||||
{ Name: 'default', IsSystem: false },
|
||||
{ Name: 'kube-system', IsSystem: true },
|
||||
@@ -188,6 +210,90 @@ describe('PersistentVolumeClaimsDatatable', () => {
|
||||
expect(screen.queryByTestId('resize-form')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Used by column', () => {
|
||||
it('shows the owning application name for PVCs with a workload', async () => {
|
||||
vi.mocked(usePersistentVolumeClaims).mockImplementation(
|
||||
(_envId, options) => {
|
||||
const data = [pvcWithWorkload];
|
||||
return {
|
||||
data: options?.select ? options.select(data) : data,
|
||||
isLoading: false,
|
||||
} as ReturnType<typeof usePersistentVolumeClaims>;
|
||||
}
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
expect(await screen.findByText('my-deployment')).toBeVisible();
|
||||
});
|
||||
|
||||
it('shows a dash for PVCs with no owning applications', async () => {
|
||||
vi.mocked(usePersistentVolumeClaims).mockImplementation(
|
||||
(_envId, options) => {
|
||||
const data = [mockPVCs[0]];
|
||||
return {
|
||||
data: options?.select ? options.select(data) : data,
|
||||
isLoading: false,
|
||||
} as ReturnType<typeof usePersistentVolumeClaims>;
|
||||
}
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await screen.findByText('test-pvc-1');
|
||||
expect(screen.getByText('-')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('row selectability', () => {
|
||||
it('disables the row checkbox for PVCs with owning applications', async () => {
|
||||
vi.mocked(usePersistentVolumeClaims).mockImplementation(
|
||||
(_envId, options) => {
|
||||
const data = [mockPVCs[0], pvcWithWorkload];
|
||||
return {
|
||||
data: options?.select ? options.select(data) : data,
|
||||
isLoading: false,
|
||||
} as ReturnType<typeof usePersistentVolumeClaims>;
|
||||
}
|
||||
);
|
||||
|
||||
const { container } = renderComponent();
|
||||
|
||||
await screen.findByText('used-pvc');
|
||||
|
||||
// skip header checkbox (index 0); row checkboxes follow in render order
|
||||
const rowCheckboxes = Array.from(
|
||||
container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')
|
||||
).slice(1);
|
||||
|
||||
const [freeCheckbox, usedCheckbox] = rowCheckboxes;
|
||||
expect(freeCheckbox).not.toBeDisabled();
|
||||
expect(usedCheckbox).toBeDisabled();
|
||||
});
|
||||
|
||||
it('allows selection of a Bound PVC that has no owning applications', async () => {
|
||||
vi.mocked(usePersistentVolumeClaims).mockImplementation(
|
||||
(_envId, options) => {
|
||||
const data = [mockPVCs[0]]; // Bound, no owningApplications
|
||||
return {
|
||||
data: options?.select ? options.select(data) : data,
|
||||
isLoading: false,
|
||||
} as ReturnType<typeof usePersistentVolumeClaims>;
|
||||
}
|
||||
);
|
||||
|
||||
const { container } = renderComponent();
|
||||
|
||||
await screen.findByText('test-pvc-1');
|
||||
|
||||
const rowCheckboxes = Array.from(
|
||||
container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')
|
||||
).slice(1);
|
||||
|
||||
expect(rowCheckboxes[0]).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('hides system namespace PVCs by default', async () => {
|
||||
vi.mocked(useNamespacesQuery).mockReturnValue({
|
||||
data: mockNamespaces,
|
||||
|
||||
@@ -68,11 +68,13 @@ export function PersistentVolumeClaimsDatatable() {
|
||||
title="Volume claims"
|
||||
titleIcon={Database}
|
||||
disableSelect={!hasWriteAuth}
|
||||
isRowSelectable={({ original: claim }) => claim.phase !== 'Bound'}
|
||||
isRowSelectable={({ original: claim }) =>
|
||||
!claim.owningApplications?.length
|
||||
}
|
||||
renderTableActions={(selectedItems) => (
|
||||
<Authorized authorizations="K8sVolumesW">
|
||||
<DeleteButton
|
||||
confirmMessage="Do you want to remove the selected volume(s)?"
|
||||
confirmMessage="Do you want to remove the selected volume claim(s)?"
|
||||
onConfirmed={() => deleteClaimsMutation.mutate(selectedItems)}
|
||||
disabled={selectedItems.length === 0}
|
||||
isLoading={deleteClaimsMutation.isLoading}
|
||||
|
||||
@@ -15,12 +15,12 @@ import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
import { Modal } from '@@/modals';
|
||||
|
||||
import { systemResourcesSettings } from '../../datatables/SystemResourcesSettings';
|
||||
import { isPersistentVolumeUsed } from '../utils';
|
||||
import {
|
||||
DefaultDatatableSettings,
|
||||
TableSettings,
|
||||
} from '../../datatables/DefaultDatatableSettings';
|
||||
import { SystemResourceDescription } from '../../datatables/SystemResourceDescription';
|
||||
import { isPersistentVolumeUsed } from '../utils';
|
||||
|
||||
import { createPersistentVolumesColumns } from './persistentVolumesColumns';
|
||||
import { PersistentVolume } from './types';
|
||||
|
||||
@@ -40,6 +40,34 @@ export function createPersistentVolumeClaimsColumns(
|
||||
helper.accessor('namespace', {
|
||||
header: 'Namespace',
|
||||
}),
|
||||
helper.accessor('owningApplications', {
|
||||
header: 'Used by',
|
||||
id: 'owningApplications',
|
||||
cell: ({ getValue }) => {
|
||||
const apps = getValue();
|
||||
if (!apps?.length) {
|
||||
return '-';
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
{apps.map((app) => (
|
||||
<Link
|
||||
key={app.Uid ?? app.Name}
|
||||
to="kubernetes.applications.application"
|
||||
params={{
|
||||
name: app.Name,
|
||||
namespace: app.ResourcePool,
|
||||
'resource-type': app.ApplicationType,
|
||||
}}
|
||||
data-cy={`pvc-owning-app-${app.Name}`}
|
||||
>
|
||||
{app.Name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
helper.accessor('phase', {
|
||||
header: 'Status',
|
||||
cell: ({ getValue }) => {
|
||||
|
||||
@@ -13,6 +13,7 @@ export function convertToVolumeViewModels(
|
||||
return {
|
||||
Applications: owningApplications.map((app) => ({
|
||||
Name: app.Name,
|
||||
ResourcePool: app.ResourcePool,
|
||||
Namespace: app.Namespace,
|
||||
Kind: app.Kind,
|
||||
})),
|
||||
|
||||
@@ -50,6 +50,8 @@ interface K8sStorageClass {
|
||||
export interface K8sVolOwningApplication {
|
||||
Uid?: string;
|
||||
Name: string;
|
||||
ResourcePool: string;
|
||||
ApplicationType?: string;
|
||||
Namespace?: string;
|
||||
Kind?: string;
|
||||
Labels?: { [key: string]: string };
|
||||
|
||||
Reference in New Issue
Block a user