feat(kubernetes-plugin): add pv and pvc rendering (#31355)

* feat: add persistent volumes rendering

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* feat: add pv status

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* feat: add volume status and volume type

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* feat: add test for pv

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* feat: add persistent volume claims

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* feat: add persistent volume status pending

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* feat: add persistent volume metadata

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* feat: add persistent volume claims metadata

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* feat: add persistent volume claims test code

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* chore: changeset

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* chore: remove gcp and gke.io

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* chore: yarn build:api-reports

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* feat: update pv to table drawer like pod

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* feat: update pvc to table drawer like pod

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* chore: remove user event at test code

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* chore: exclude pv,pvc from default objects

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* chore: remove duplication import for pv

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* chore: yarn build:api-reports

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* chore: retrigger required checks

Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* chore: retrigger required checks

Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* chore: retrigger required checks

Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

* chore: retrigger required checks

Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>

---------

Signed-off-by: 김병준 <kingbj0429@gmail.com>
Signed-off-by: KoB <63000843+kingbj940429@users.noreply.github.com>
This commit is contained in:
KoB
2026-01-29 04:31:01 +09:00
committed by GitHub
parent 5af9e14bb0
commit ce3639c588
29 changed files with 1773 additions and 4 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-kubernetes-backend': patch
'@backstage/plugin-kubernetes-common': patch
'@backstage/plugin-kubernetes-react': patch
'@backstage/plugin-kubernetes-node': patch
---
Add PersistentVolume and PersistentVolumeClaims Rendering
@@ -148,6 +148,18 @@ export const ALL_OBJECTS: ObjectToFetch[] = [
plural: 'secrets',
objectType: 'secrets',
},
{
group: '',
apiVersion: 'v1',
plural: 'persistentvolumes',
objectType: 'persistentvolumes',
},
{
group: '',
apiVersion: 'v1',
plural: 'persistentvolumeclaims',
objectType: 'persistentvolumeclaims',
},
...DEFAULT_OBJECTS,
];
+25 -1
View File
@@ -17,6 +17,8 @@ import type { V1Deployment } from '@kubernetes/client-node';
import type { V1Ingress } from '@kubernetes/client-node';
import type { V1Job } from '@kubernetes/client-node';
import type { V1LimitRange } from '@kubernetes/client-node';
import type { V1PersistentVolume } from '@kubernetes/client-node';
import type { V1PersistentVolumeClaim } from '@kubernetes/client-node';
import type { V1Pod } from '@kubernetes/client-node';
import type { V1ReplicaSet } from '@kubernetes/client-node';
import type { V1ResourceQuota } from '@kubernetes/client-node';
@@ -267,7 +269,9 @@ export type FetchResponse =
| StatefulSetsFetchResponse
| DaemonSetsFetchResponse
| PodStatusFetchResponse
| SecretsFetchResponse;
| SecretsFetchResponse
| PersistentVolumeFetchResponse
| PersistentVolumeClaimsFetchResponse;
// @public (undocumented)
export interface GroupedResponses extends DeploymentResources {
@@ -284,6 +288,10 @@ export interface GroupedResponses extends DeploymentResources {
// (undocumented)
jobs: V1Job[];
// (undocumented)
persistentVolumeClaims: V1PersistentVolumeClaim[];
// (undocumented)
persistentVolumes: V1PersistentVolume[];
// (undocumented)
secrets: V1Secret[];
// (undocumented)
services: V1Service[];
@@ -385,6 +393,22 @@ export interface ObjectsByEntityResponse {
items: ClusterObjects[];
}
// @public (undocumented)
export interface PersistentVolumeClaimsFetchResponse {
// (undocumented)
resources: Array<V1PersistentVolumeClaim>;
// (undocumented)
type: 'persistentvolumeclaims';
}
// @public (undocumented)
export interface PersistentVolumeFetchResponse {
// (undocumented)
resources: Array<V1PersistentVolume>;
// (undocumented)
type: 'persistentvolumes';
}
// @public (undocumented)
export interface PodFetchResponse {
// (undocumented)
+19 -1
View File
@@ -31,6 +31,8 @@ import type {
V1Service,
V1StatefulSet,
V1Secret,
V1PersistentVolume,
V1PersistentVolumeClaim,
} from '@kubernetes/client-node';
import { Entity } from '@backstage/catalog-model';
@@ -142,7 +144,9 @@ export type FetchResponse =
| StatefulSetsFetchResponse
| DaemonSetsFetchResponse
| PodStatusFetchResponse
| SecretsFetchResponse;
| SecretsFetchResponse
| PersistentVolumeFetchResponse
| PersistentVolumeClaimsFetchResponse;
/** @public */
export interface PodFetchResponse {
@@ -246,6 +250,18 @@ export interface SecretsFetchResponse {
resources: Array<V1Secret>;
}
/** @public */
export interface PersistentVolumeFetchResponse {
type: 'persistentvolumes';
resources: Array<V1PersistentVolume>;
}
/** @public */
export interface PersistentVolumeClaimsFetchResponse {
type: 'persistentvolumeclaims';
resources: Array<V1PersistentVolumeClaim>;
}
/** @public */
export type KubernetesFetchError = StatusError | RawFetchError;
@@ -311,4 +327,6 @@ export interface GroupedResponses extends DeploymentResources {
customResources: any[];
statefulsets: V1StatefulSet[];
daemonSets: V1DaemonSet[];
persistentVolumes: V1PersistentVolume[];
persistentVolumeClaims: V1PersistentVolumeClaim[];
}
@@ -64,6 +64,12 @@ export const groupResponses = (
case 'daemonsets':
prev.daemonSets.push(...next.resources);
break;
case 'persistentvolumes':
prev.persistentVolumes.push(...next.resources);
break;
case 'persistentvolumeclaims':
prev.persistentVolumeClaims.push(...next.resources);
break;
default:
}
return prev;
@@ -82,6 +88,8 @@ export const groupResponses = (
customResources: [],
statefulsets: [],
daemonSets: [],
persistentVolumes: [],
persistentVolumeClaims: [],
} as GroupedResponses,
);
};
+3 -1
View File
@@ -221,7 +221,9 @@ export type KubernetesObjectTypes =
| 'customresources'
| 'statefulsets'
| 'daemonsets'
| 'secrets';
| 'secrets'
| 'persistentvolumes'
| 'persistentvolumeclaims';
// @public
export interface KubernetesRouterExtensionPoint {
+3 -1
View File
@@ -187,7 +187,9 @@ export type KubernetesObjectTypes =
| 'customresources'
| 'statefulsets'
| 'daemonsets'
| 'secrets';
| 'secrets'
| 'persistentvolumes'
| 'persistentvolumeclaims';
// If updating this list, also make sure to update
// `objectTypes` and `apiVersionOverrides` in config.d.ts on @backstage/plugin-kubernetes-backend!
@@ -0,0 +1,37 @@
{
"persistentVolumeClaims": [
{
"apiVersion": "v1",
"kind": "PersistentVolumeClaim",
"metadata": {
"name": "pvc-web-storage",
"namespace": "default",
"uid": "1ea073bc-7a4b-4b99-8321-0305bce85568",
"resourceVersion": "1362732552",
"creationTimestamp": "2021-07-16T22:39:58Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "web-app"
},
"annotations": {}
},
"spec": {
"accessModes": ["ReadWriteOnce"],
"resources": {
"requests": {
"storage": "10Gi"
}
},
"storageClassName": "standard",
"volumeName": "pv-web-volume"
},
"status": {
"phase": "Bound",
"accessModes": ["ReadWriteOnce"],
"capacity": {
"storage": "10Gi"
}
}
}
]
}
@@ -0,0 +1,33 @@
{
"persistentVolumes": [
{
"apiVersion": "v1",
"kind": "PersistentVolume",
"metadata": {
"name": "pv-hostpath",
"uid": "1ea073bc-7a4b-4b99-8321-0305bce85568",
"resourceVersion": "1362732552",
"creationTimestamp": "2021-07-16T22:39:58Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"type": "local"
},
"annotations": {}
},
"spec": {
"capacity": {
"storage": "10Gi"
},
"accessModes": ["ReadWriteOnce"],
"persistentVolumeReclaimPolicy": "Retain",
"storageClassName": "manual",
"hostPath": {
"path": "/tmp/data"
}
},
"status": {
"phase": "Available"
}
}
]
}
@@ -0,0 +1,160 @@
{
"persistentVolumeClaims": [
{
"apiVersion": "v1",
"kind": "PersistentVolumeClaim",
"metadata": {
"name": "pvc-web-storage",
"namespace": "default",
"uid": "1ea073bc-7a4b-4b99-8321-0305bce85568",
"resourceVersion": "1362732552",
"creationTimestamp": "2021-07-16T22:39:58Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "web-app"
},
"annotations": {}
},
"spec": {
"accessModes": ["ReadWriteOnce"],
"resources": {
"requests": {
"storage": "10Gi"
}
},
"storageClassName": "standard",
"volumeName": "pv-web-volume"
},
"status": {
"phase": "Bound",
"accessModes": ["ReadWriteOnce"],
"capacity": {
"storage": "10Gi"
}
}
},
{
"apiVersion": "v1",
"kind": "PersistentVolumeClaim",
"metadata": {
"name": "pvc-database-storage",
"namespace": "production",
"uid": "2fb184cd-8b5c-5c10-9432-1416cdf96679",
"resourceVersion": "1362732553",
"creationTimestamp": "2021-07-17T10:20:15Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "database"
},
"annotations": {}
},
"spec": {
"accessModes": ["ReadWriteOnce"],
"resources": {
"requests": {
"storage": "50Gi"
}
},
"storageClassName": "fast-ssd",
"volumeName": "pv-database-volume"
},
"status": {
"phase": "Bound",
"accessModes": ["ReadWriteOnce"],
"capacity": {
"storage": "50Gi"
}
}
},
{
"apiVersion": "v1",
"kind": "PersistentVolumeClaim",
"metadata": {
"name": "pvc-cache-storage",
"namespace": "staging",
"uid": "3gc295de-9c6d-6d21-a543-2527def07790",
"resourceVersion": "1362732554",
"creationTimestamp": "2021-07-18T15:30:45Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "cache-service"
},
"annotations": {}
},
"spec": {
"accessModes": ["ReadWriteMany"],
"resources": {
"requests": {
"storage": "5Gi"
}
},
"storageClassName": "shared-storage"
},
"status": {
"phase": "Pending"
}
},
{
"apiVersion": "v1",
"kind": "PersistentVolumeClaim",
"metadata": {
"name": "pvc-logs-storage",
"namespace": "logging",
"uid": "4hd3a6ef-ad7e-7e32-b654-3638eff18801",
"resourceVersion": "1362732555",
"creationTimestamp": "2021-07-19T09:15:30Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "log-aggregator"
},
"annotations": {}
},
"spec": {
"accessModes": ["ReadWriteMany"],
"resources": {
"requests": {
"storage": "100Gi"
}
},
"storageClassName": "nfs-storage",
"volumeName": "pv-logs-volume"
},
"status": {
"phase": "Bound",
"accessModes": ["ReadWriteMany"],
"capacity": {
"storage": "100Gi"
}
}
},
{
"apiVersion": "v1",
"kind": "PersistentVolumeClaim",
"metadata": {
"name": "pvc-backup-storage",
"namespace": "backup",
"uid": "5ie4b7f0-be8f-8f43-c765-4749f0029912",
"resourceVersion": "1362732556",
"creationTimestamp": "2021-07-20T16:45:22Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "backup-service"
},
"annotations": {}
},
"spec": {
"accessModes": ["ReadWriteOnce"],
"resources": {
"requests": {
"storage": "200Gi"
}
},
"storageClassName": "backup-storage",
"volumeName": "pv-backup-volume-deleted"
},
"status": {
"phase": "Lost"
}
}
]
}
@@ -0,0 +1,274 @@
{
"persistentVolumes": [
{
"apiVersion": "v1",
"kind": "PersistentVolume",
"metadata": {
"name": "pv-aws-ebs",
"uid": "1ea073bc-7a4b-4b99-8321-0305bce85568",
"resourceVersion": "1362732552",
"creationTimestamp": "2021-07-16T22:39:58Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "dice-roller"
},
"annotations": {}
},
"spec": {
"capacity": {
"storage": "100Gi"
},
"accessModes": ["ReadWriteOnce"],
"persistentVolumeReclaimPolicy": "Delete",
"storageClassName": "gp3",
"csi": {
"driver": "ebs.csi.aws.com",
"volumeHandle": "vol-1234567890abcdef0"
}
},
"status": {
"phase": "Bound"
}
},
{
"apiVersion": "v1",
"kind": "PersistentVolume",
"metadata": {
"name": "pv-aws-efs",
"uid": "2fb184cd-8b5c-5c10-9432-1416cdf96679",
"resourceVersion": "1362732553",
"creationTimestamp": "2021-07-17T10:20:15Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "dice-roller"
},
"annotations": {}
},
"spec": {
"capacity": {
"storage": "5Ti"
},
"accessModes": ["ReadWriteMany"],
"persistentVolumeReclaimPolicy": "Retain",
"storageClassName": "efs-sc",
"csi": {
"driver": "efs.csi.aws.com",
"volumeHandle": "fs-0123456789abcdef0"
}
},
"status": {
"phase": "Available"
}
},
{
"apiVersion": "v1",
"kind": "PersistentVolume",
"metadata": {
"name": "pv-aws-s3",
"uid": "3gc295de-9c6d-6d21-a543-2527def07790",
"resourceVersion": "1362732554",
"creationTimestamp": "2021-07-18T08:45:30Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "dice-roller"
},
"annotations": {}
},
"spec": {
"capacity": {
"storage": "10Ti"
},
"accessModes": ["ReadWriteMany"],
"persistentVolumeReclaimPolicy": "Retain",
"storageClassName": "s3-csi",
"csi": {
"driver": "s3.csi.aws.com",
"volumeHandle": "my-s3-bucket"
}
},
"status": {
"phase": "Bound"
}
},
{
"apiVersion": "v1",
"kind": "PersistentVolume",
"metadata": {
"name": "pv-gcp-pd",
"uid": "4hd3a6ef-ad7e-7e32-b654-3638eff18801",
"resourceVersion": "1362732555",
"creationTimestamp": "2021-07-18T15:30:45Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "dice-roller"
},
"annotations": {}
},
"spec": {
"capacity": {
"storage": "200Gi"
},
"accessModes": ["ReadWriteOnce"],
"persistentVolumeReclaimPolicy": "Delete",
"storageClassName": "ssd",
"csi": {
"driver": "pd.csi.storage.gke.io",
"volumeHandle": "projects/my-project/zones/us-central1-a/disks/my-disk"
}
},
"status": {
"phase": "Bound"
}
},
{
"apiVersion": "v1",
"kind": "PersistentVolume",
"metadata": {
"name": "pv-gcp-filestore",
"uid": "5ie4b7f0-be8f-8f43-c765-4749f0029912",
"resourceVersion": "1362732556",
"creationTimestamp": "2021-07-19T09:15:30Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "dice-roller"
},
"annotations": {}
},
"spec": {
"capacity": {
"storage": "1Ti"
},
"accessModes": ["ReadWriteMany"],
"persistentVolumeReclaimPolicy": "Retain",
"storageClassName": "filestore-csi",
"csi": {
"driver": "filestore.csi.storage.gke.io",
"volumeHandle": "projects/my-project/locations/us-central1-c/instances/my-filestore"
}
},
"status": {
"phase": "Available"
}
},
{
"apiVersion": "v1",
"kind": "PersistentVolume",
"metadata": {
"name": "pv-gcp-gcsfuse",
"uid": "6jf5c8g1-cf90-9054-d876-5850g1130a23",
"resourceVersion": "1362732557",
"creationTimestamp": "2021-07-20T12:30:45Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "dice-roller"
},
"annotations": {}
},
"spec": {
"capacity": {
"storage": "50Ti"
},
"accessModes": ["ReadWriteMany"],
"persistentVolumeReclaimPolicy": "Retain",
"storageClassName": "gcsfuse",
"csi": {
"driver": "gcsfuse.csi.storage.gke.io",
"volumeHandle": "my-gcs-bucket"
}
},
"status": {
"phase": "Available"
}
},
{
"apiVersion": "v1",
"kind": "PersistentVolume",
"metadata": {
"name": "pv-azure-disk",
"uid": "7kg6d9h2-dg01-0165-e987-6961h2241b34",
"resourceVersion": "1362732558",
"creationTimestamp": "2021-07-20T14:45:20Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "dice-roller"
},
"annotations": {}
},
"spec": {
"capacity": {
"storage": "128Gi"
},
"accessModes": ["ReadWriteOnce"],
"persistentVolumeReclaimPolicy": "Delete",
"storageClassName": "managed-premium",
"csi": {
"driver": "disk.csi.azure.com",
"volumeHandle": "/subscriptions/subscription-id/resourceGroups/rg/providers/Microsoft.Compute/disks/my-disk"
}
},
"status": {
"phase": "Bound"
}
},
{
"apiVersion": "v1",
"kind": "PersistentVolume",
"metadata": {
"name": "pv-azure-file",
"uid": "8lh7e0i3-eh12-1276-f098-7072i3352c45",
"resourceVersion": "1362732559",
"creationTimestamp": "2021-07-21T11:20:10Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "dice-roller"
},
"annotations": {}
},
"spec": {
"capacity": {
"storage": "100Gi"
},
"accessModes": ["ReadWriteMany"],
"persistentVolumeReclaimPolicy": "Retain",
"storageClassName": "azurefile",
"csi": {
"driver": "file.csi.azure.com",
"volumeHandle": "unique-volumeid"
}
},
"status": {
"phase": "Available"
}
},
{
"apiVersion": "v1",
"kind": "PersistentVolume",
"metadata": {
"name": "pv-azure-blob",
"uid": "9mi8f1j4-fi23-2387-g109-8183j4463d56",
"resourceVersion": "1362732560",
"creationTimestamp": "2021-07-22T16:30:25Z",
"labels": {
"backstage.io/kubernetes-id": "dice-roller",
"app": "dice-roller"
},
"annotations": {}
},
"spec": {
"capacity": {
"storage": "500Gi"
},
"accessModes": ["ReadWriteMany"],
"persistentVolumeReclaimPolicy": "Delete",
"storageClassName": "azureblob",
"csi": {
"driver": "blob.csi.azure.com",
"volumeHandle": "my-storage-account/my-container"
}
},
"status": {
"phase": "Bound"
}
}
]
}
@@ -34,6 +34,8 @@ import { ConfigmapsAccordions } from '../ConfigmapsAccordions';
import { CronJobsAccordions } from '../CronJobsAccordions';
import { CustomResources } from '../CustomResources';
import { DaemonSetsAccordions } from '../DaemonSetsAccordions';
import { PersistentVolumesAccordions } from '../PersistentVolumesAccordions';
import { PersistentVolumeClaimsAccordions } from '../PersistentVolumesClaimsAccordions';
import {
ClusterContext,
GroupedResponsesContext,
@@ -187,6 +189,16 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => {
<SecretsAccordions />
</Grid>
) : undefined}
{groupedResponses.persistentVolumes.length > 0 ? (
<Grid item>
<PersistentVolumesAccordions />
</Grid>
) : undefined}
{groupedResponses.persistentVolumeClaims.length > 0 ? (
<Grid item>
<PersistentVolumeClaimsAccordions />
</Grid>
) : undefined}
{groupedResponses.cronJobs.length > 0 ? (
<Grid item>
<CronJobsAccordions />
@@ -0,0 +1,50 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { screen } from '@testing-library/react';
import { PersistentVolumesAccordions } from './PersistentVolumesAccordions';
import * as onePersistentVolumesFixture from '../../__fixtures__/1-persistentvolumes.json';
import * as twoPersistentVolumesFixture from '../../__fixtures__/2-persistentvolumes.json';
import { renderInTestApp } from '@backstage/test-utils';
import { kubernetesProviders } from '../../hooks/test-utils';
describe('PersistentVolumesAccordions', () => {
it('should render 1 persistent volume with summary', async () => {
const wrapper = kubernetesProviders(
onePersistentVolumesFixture,
new Set<string>(),
);
await renderInTestApp(wrapper(<PersistentVolumesAccordions />));
expect(screen.getByText('PersistentVolumes')).toBeInTheDocument();
expect(screen.getByText('1 volumes')).toBeInTheDocument();
expect(screen.getByText('0 bound')).toBeInTheDocument();
});
it('should render 9 persistent volumes with summary', async () => {
const wrapper = kubernetesProviders(
twoPersistentVolumesFixture,
new Set<string>(),
);
await renderInTestApp(wrapper(<PersistentVolumesAccordions />));
expect(screen.getByText('PersistentVolumes')).toBeInTheDocument();
expect(screen.getByText('9 volumes')).toBeInTheDocument();
expect(screen.getByText('5 bound')).toBeInTheDocument();
});
});
@@ -0,0 +1,137 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ReactNode, useContext } from 'react';
import Accordion from '@material-ui/core/Accordion';
import AccordionDetails from '@material-ui/core/AccordionDetails';
import AccordionSummary from '@material-ui/core/AccordionSummary';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import type { V1PersistentVolume } from '@kubernetes/client-node';
import { PersistentVolumesTable } from './PersistentVolumesTable';
import { GroupedResponsesContext } from '../../hooks';
import { StatusOK, StatusError } from '@backstage/core-components';
type PersistentVolumesAccordionsProps = {
children?: ReactNode;
};
type PersistentVolumesAccordionProps = {
persistentVolumes: V1PersistentVolume[];
children?: ReactNode;
};
type PersistentVolumesSummaryProps = {
numberOfPersistentVolumes: number;
numberOfBoundVolumes: number;
numberOfFailedVolumes: number;
children?: ReactNode;
};
const PersistentVolumesSummary = ({
numberOfPersistentVolumes,
numberOfBoundVolumes,
numberOfFailedVolumes,
}: PersistentVolumesSummaryProps) => {
return (
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={0}
>
<Grid xs={4} item>
<Typography variant="body1">
<strong>PersistentVolumes</strong>
</Typography>
</Grid>
<Grid
item
container
xs={8}
direction="column"
justifyContent="flex-start"
alignItems="flex-end"
spacing={0}
>
<Grid item>
<StatusOK>{numberOfPersistentVolumes} volumes</StatusOK>
</Grid>
<Grid item>
<StatusOK>{numberOfBoundVolumes} bound</StatusOK>
</Grid>
{numberOfFailedVolumes > 0 && (
<Grid item>
<StatusError>
{numberOfFailedVolumes} volume
{numberOfFailedVolumes > 1 ? 's' : ''} failed
</StatusError>
</Grid>
)}
</Grid>
</Grid>
);
};
const PersistentVolumesAccordion = ({
persistentVolumes,
}: PersistentVolumesAccordionProps) => {
const boundVolumes = persistentVolumes.filter(
pv => pv.status?.phase === 'Bound',
);
const failedVolumes = persistentVolumes.filter(
pv => pv.status?.phase === 'Failed',
);
return (
<Accordion TransitionProps={{ unmountOnExit: true }} variant="outlined">
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<PersistentVolumesSummary
numberOfPersistentVolumes={persistentVolumes.length}
numberOfBoundVolumes={boundVolumes.length}
numberOfFailedVolumes={failedVolumes.length}
/>
</AccordionSummary>
<AccordionDetails>
<PersistentVolumesTable persistentVolumes={persistentVolumes} />
</AccordionDetails>
</Accordion>
);
};
export const PersistentVolumesAccordions =
({}: PersistentVolumesAccordionsProps) => {
const groupedResponses = useContext(GroupedResponsesContext);
return (
<Grid
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid container item xs>
<Grid item xs>
<PersistentVolumesAccordion
persistentVolumes={groupedResponses.persistentVolumes}
/>
</Grid>
</Grid>
</Grid>
);
};
@@ -0,0 +1,38 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as onePersistentVolumesFixture from '../../__fixtures__/1-persistentvolumes.json';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { PersistentVolumesDrawer } from './PersistentVolumesDrawer';
import { kubernetesClusterLinkFormatterApiRef } from '../../api';
describe('PersistentVolumesDrawer', () => {
it('should render persistent volume drawer', async () => {
const { getByText, getAllByText } = await renderInTestApp(
<TestApiProvider apis={[[kubernetesClusterLinkFormatterApiRef, {}]]}>
<PersistentVolumesDrawer
persistentVolume={
(onePersistentVolumesFixture as any).persistentVolumes[0]
}
expanded
/>
</TestApiProvider>,
);
expect(getAllByText('pv-hostpath')).toHaveLength(3);
expect(getAllByText('PersistentVolume')).toHaveLength(3);
expect(getByText('YAML')).toBeInTheDocument();
});
});
@@ -0,0 +1,64 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { V1PersistentVolume } from '@kubernetes/client-node';
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
import Typography from '@material-ui/core/Typography';
import Grid from '@material-ui/core/Grid';
import Chip from '@material-ui/core/Chip';
export const PersistentVolumesDrawer = ({
persistentVolume,
expanded,
}: {
persistentVolume: V1PersistentVolume;
expanded?: boolean;
}) => {
const namespace = persistentVolume.metadata?.namespace;
return (
<KubernetesStructuredMetadataTableDrawer
object={persistentVolume}
expanded={expanded}
kind="PersistentVolume"
renderObject={(persistentVolumeObject: V1PersistentVolume) => {
return persistentVolumeObject || {};
}}
>
<Grid
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
spacing={0}
>
<Grid item>
<Typography variant="body1">
{persistentVolume.metadata?.name ?? 'unknown object'}
</Typography>
</Grid>
<Grid item>
<Typography color="textSecondary" variant="subtitle1">
PersistentVolume
</Typography>
</Grid>
{namespace && (
<Grid item>
<Chip size="small" label={`namespace: ${namespace}`} />
</Grid>
)}
</Grid>
</KubernetesStructuredMetadataTableDrawer>
);
};
@@ -0,0 +1,81 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { screen } from '@testing-library/react';
import * as onePersistentVolumesFixture from '../../__fixtures__/1-persistentvolumes.json';
import * as twoPersistentVolumesFixture from '../../__fixtures__/2-persistentvolumes.json';
import { renderInTestApp } from '@backstage/test-utils';
import { PersistentVolumesTable } from './PersistentVolumesTable';
describe('PersistentVolumesTable', () => {
it('should render persistent volume table with columns', async () => {
await renderInTestApp(
<PersistentVolumesTable
persistentVolumes={
(onePersistentVolumesFixture as any).persistentVolumes
}
/>,
);
expect(screen.getByText('name')).toBeInTheDocument();
expect(screen.getByText('phase')).toBeInTheDocument();
expect(screen.getByText('status')).toBeInTheDocument();
expect(screen.getByText('capacity')).toBeInTheDocument();
expect(screen.getByText('type')).toBeInTheDocument();
expect(screen.getByText('claim')).toBeInTheDocument();
expect(screen.getByText('pv-hostpath')).toBeInTheDocument();
expect(screen.getAllByText('Available').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('10Gi')).toBeInTheDocument();
});
it('should render multiple persistent volumes with cloud provider types', async () => {
await renderInTestApp(
<PersistentVolumesTable
persistentVolumes={
(twoPersistentVolumesFixture as any).persistentVolumes
}
/>,
);
expect(screen.getByText('name')).toBeInTheDocument();
expect(screen.getByText('phase')).toBeInTheDocument();
expect(screen.getByText('status')).toBeInTheDocument();
expect(screen.getByText('capacity')).toBeInTheDocument();
expect(screen.getByText('type')).toBeInTheDocument();
expect(screen.getByText('claim')).toBeInTheDocument();
expect(screen.getByText('pv-aws-ebs')).toBeInTheDocument();
expect(screen.getByText('pv-aws-efs')).toBeInTheDocument();
expect(screen.getByText('pv-aws-s3')).toBeInTheDocument();
expect(screen.getByText('pv-gcp-pd')).toBeInTheDocument();
expect(screen.getByText('pv-gcp-filestore')).toBeInTheDocument();
expect(screen.getByText('AWS EBS Volume')).toBeInTheDocument();
expect(screen.getByText('AWS EFS')).toBeInTheDocument();
expect(screen.getByText('S3 Bucket')).toBeInTheDocument();
expect(screen.getByText('GCP Persistent Disk')).toBeInTheDocument();
expect(screen.getByText('GCP Filestore')).toBeInTheDocument();
expect(screen.getAllByText('Bound').length).toBeGreaterThanOrEqual(3);
expect(screen.getAllByText('Available').length).toBeGreaterThanOrEqual(2);
expect(screen.getByText('100Gi')).toBeInTheDocument();
expect(screen.getByText('5Ti')).toBeInTheDocument();
expect(screen.getByText('10Ti')).toBeInTheDocument();
expect(screen.getByText('200Gi')).toBeInTheDocument();
});
});
@@ -0,0 +1,152 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ReactNode } from 'react';
import { PersistentVolumesDrawer } from './PersistentVolumesDrawer';
import { Table, TableColumn } from '@backstage/core-components';
import type { V1PersistentVolume } from '@kubernetes/client-node';
import {
StatusError,
StatusOK,
StatusPending,
} from '@backstage/core-components';
import { getPersistentVolumeType } from '../../utils/persistentVolumes';
export type PersistentVolumesTableProps = {
persistentVolumes: V1PersistentVolume[];
children?: ReactNode;
};
const PersistentVolumeDrawerTrigger = ({
persistentVolume,
}: {
persistentVolume: V1PersistentVolume;
}) => {
return <PersistentVolumesDrawer persistentVolume={persistentVolume} />;
};
const renderPhaseStatus = (persistentVolume: V1PersistentVolume) => {
const phase = persistentVolume.status?.phase;
if (phase === 'Bound') {
return <StatusOK>Bound</StatusOK>;
}
if (phase === 'Available') {
return <StatusPending>Available</StatusPending>;
}
if (phase === 'Released') {
return <StatusPending>Released</StatusPending>;
}
if (phase === 'Pending') {
return <StatusPending>Pending</StatusPending>;
}
if (phase === 'Failed') {
return <StatusError>Failed</StatusError>;
}
return <>{phase ?? 'Unknown'}</>;
};
export const PersistentVolumesTable = ({
persistentVolumes,
}: PersistentVolumesTableProps) => {
const defaultColumns: TableColumn<V1PersistentVolume>[] = [
{
title: 'ID',
field: 'metadata.uid',
hidden: true,
},
{
title: 'name',
highlight: true,
render: (persistentVolume: V1PersistentVolume) => {
return (
<PersistentVolumeDrawerTrigger persistentVolume={persistentVolume} />
);
},
cellStyle: {
width: '20%',
},
},
{
title: 'phase',
render: renderPhaseStatus,
cellStyle: {
width: '9%',
},
},
{
title: 'status',
render: (persistentVolume: V1PersistentVolume) =>
persistentVolume.status?.phase ?? 'Unknown',
cellStyle: {
width: '9%',
},
},
{
title: 'capacity',
render: (persistentVolume: V1PersistentVolume) =>
persistentVolume.spec?.capacity?.storage ?? 'N/A',
cellStyle: {
width: '10%',
},
},
{
title: 'type',
render: (persistentVolume: V1PersistentVolume) => {
const driver = persistentVolume.spec?.csi?.driver;
return getPersistentVolumeType(driver) ?? 'N/A';
},
cellStyle: {
width: '15%',
},
},
{
title: 'claim',
render: (persistentVolume: V1PersistentVolume) => {
const claim = persistentVolume.spec?.claimRef;
if (claim?.namespace && claim?.name) {
return `${claim.namespace}/${claim.name}`;
}
return 'N/A';
},
cellStyle: {
width: '37%',
},
},
];
const columns: TableColumn<V1PersistentVolume>[] = [...defaultColumns];
const tableStyle = {
minWidth: '0',
width: '100%',
};
return (
<div style={tableStyle}>
<Table
options={{ paging: true, search: false, emptyRowsWhenPaging: false }}
data={
persistentVolumes.map((pv: V1PersistentVolume) => ({
...pv,
id: pv?.metadata?.uid,
})) as any as V1PersistentVolume[]
}
columns={columns}
/>
</div>
);
};
@@ -0,0 +1,18 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './PersistentVolumesAccordions';
export * from './PersistentVolumesTable';
export * from './PersistentVolumesDrawer';
@@ -0,0 +1,51 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { screen } from '@testing-library/react';
import { PersistentVolumeClaimsAccordions } from './PersistentVolumeClaimsAccordions';
import * as onePersistentVolumeClaimsFixture from '../../__fixtures__/1-persistentvolumeclaims.json';
import * as twoPersistentVolumeClaimsFixture from '../../__fixtures__/2-persistentvolumeclaims.json';
import { renderInTestApp } from '@backstage/test-utils';
import { kubernetesProviders } from '../../hooks/test-utils';
describe('PersistentVolumeClaimsAccordions', () => {
it('should render 1 persistent volume claim with summary', async () => {
const wrapper = kubernetesProviders(
onePersistentVolumeClaimsFixture,
new Set<string>(),
);
await renderInTestApp(wrapper(<PersistentVolumeClaimsAccordions />));
expect(screen.getByText('PersistentVolumeClaims')).toBeInTheDocument();
expect(screen.getByText('1 claims')).toBeInTheDocument();
expect(screen.getByText('1 bound')).toBeInTheDocument();
});
it('should render multiple persistent volume claims with summary', async () => {
const wrapper = kubernetesProviders(
twoPersistentVolumeClaimsFixture,
new Set<string>(),
);
await renderInTestApp(wrapper(<PersistentVolumeClaimsAccordions />));
expect(screen.getByText('PersistentVolumeClaims')).toBeInTheDocument();
expect(screen.getByText('5 claims')).toBeInTheDocument();
expect(screen.getByText('3 bound')).toBeInTheDocument();
expect(screen.getByText('1 claim lost')).toBeInTheDocument();
});
});
@@ -0,0 +1,139 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ReactNode, useContext } from 'react';
import Accordion from '@material-ui/core/Accordion';
import AccordionDetails from '@material-ui/core/AccordionDetails';
import AccordionSummary from '@material-ui/core/AccordionSummary';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import type { V1PersistentVolumeClaim } from '@kubernetes/client-node';
import { PersistentVolumeClaimsTable } from './PersistentVolumeClaimsTable';
import { GroupedResponsesContext } from '../../hooks';
import { StatusOK, StatusError } from '@backstage/core-components';
type PersistentVolumeClaimsAccordionsProps = {
children?: ReactNode;
};
type PersistentVolumeClaimsAccordionProps = {
persistentVolumeClaims: V1PersistentVolumeClaim[];
children?: ReactNode;
};
type PersistentVolumeClaimsSummaryProps = {
numberOfPersistentVolumeClaims: number;
numberOfBoundClaims: number;
numberOfLostClaims: number;
children?: ReactNode;
};
const PersistentVolumeClaimsSummary = ({
numberOfPersistentVolumeClaims,
numberOfBoundClaims,
numberOfLostClaims,
}: PersistentVolumeClaimsSummaryProps) => {
return (
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={0}
>
<Grid xs={4} item>
<Typography variant="body1">
<strong>PersistentVolumeClaims</strong>
</Typography>
</Grid>
<Grid
item
container
xs={8}
direction="column"
justifyContent="flex-start"
alignItems="flex-end"
spacing={0}
>
<Grid item>
<StatusOK>{numberOfPersistentVolumeClaims} claims</StatusOK>
</Grid>
<Grid item>
<StatusOK>{numberOfBoundClaims} bound</StatusOK>
</Grid>
{numberOfLostClaims > 0 && (
<Grid item>
<StatusError>
{numberOfLostClaims} claim
{numberOfLostClaims > 1 ? 's' : ''} lost
</StatusError>
</Grid>
)}
</Grid>
</Grid>
);
};
const PersistentVolumeClaimsAccordion = ({
persistentVolumeClaims,
}: PersistentVolumeClaimsAccordionProps) => {
const boundClaims = persistentVolumeClaims.filter(
pvc => pvc.status?.phase === 'Bound',
);
const lostClaims = persistentVolumeClaims.filter(
pvc => pvc.status?.phase === 'Lost',
);
return (
<Accordion TransitionProps={{ unmountOnExit: true }} variant="outlined">
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<PersistentVolumeClaimsSummary
numberOfPersistentVolumeClaims={persistentVolumeClaims.length}
numberOfBoundClaims={boundClaims.length}
numberOfLostClaims={lostClaims.length}
/>
</AccordionSummary>
<AccordionDetails>
<PersistentVolumeClaimsTable
persistentVolumeClaims={persistentVolumeClaims}
/>
</AccordionDetails>
</Accordion>
);
};
export const PersistentVolumeClaimsAccordions =
({}: PersistentVolumeClaimsAccordionsProps) => {
const groupedResponses = useContext(GroupedResponsesContext);
return (
<Grid
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid container item xs>
<Grid item xs>
<PersistentVolumeClaimsAccordion
persistentVolumeClaims={groupedResponses.persistentVolumeClaims}
/>
</Grid>
</Grid>
</Grid>
);
};
@@ -0,0 +1,39 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as onePersistentVolumeClaimsFixture from '../../__fixtures__/1-persistentvolumeclaims.json';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { PersistentVolumeClaimsDrawer } from './PersistentVolumeClaimsDrawer';
import { kubernetesClusterLinkFormatterApiRef } from '../../api';
describe('PersistentVolumeClaimsDrawer', () => {
it('should render persistent volume claim drawer', async () => {
const { getByText, getAllByText } = await renderInTestApp(
<TestApiProvider apis={[[kubernetesClusterLinkFormatterApiRef, {}]]}>
<PersistentVolumeClaimsDrawer
persistentVolumeClaim={
(onePersistentVolumeClaimsFixture as any).persistentVolumeClaims[0]
}
expanded
/>
</TestApiProvider>,
);
expect(getAllByText('pvc-web-storage')).toHaveLength(3);
expect(getAllByText('PersistentVolumeClaim')).toHaveLength(3);
expect(getByText('YAML')).toBeInTheDocument();
expect(getByText('namespace: default')).toBeInTheDocument();
});
});
@@ -0,0 +1,64 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { V1PersistentVolumeClaim } from '@kubernetes/client-node';
import { KubernetesStructuredMetadataTableDrawer } from '../KubernetesDrawer';
import Typography from '@material-ui/core/Typography';
import Grid from '@material-ui/core/Grid';
import Chip from '@material-ui/core/Chip';
export const PersistentVolumeClaimsDrawer = ({
persistentVolumeClaim,
expanded,
}: {
persistentVolumeClaim: V1PersistentVolumeClaim;
expanded?: boolean;
}) => {
const namespace = persistentVolumeClaim.metadata?.namespace;
return (
<KubernetesStructuredMetadataTableDrawer
object={persistentVolumeClaim}
expanded={expanded}
kind="PersistentVolumeClaim"
renderObject={(persistentVolumeClaimObject: V1PersistentVolumeClaim) => {
return persistentVolumeClaimObject || {};
}}
>
<Grid
container
direction="column"
justifyContent="flex-start"
alignItems="flex-start"
spacing={0}
>
<Grid item>
<Typography variant="body1">
{persistentVolumeClaim.metadata?.name ?? 'unknown object'}
</Typography>
</Grid>
<Grid item>
<Typography color="textSecondary" variant="subtitle1">
PersistentVolumeClaim
</Typography>
</Grid>
{namespace && (
<Grid item>
<Chip size="small" label={`namespace: ${namespace}`} />
</Grid>
)}
</Grid>
</KubernetesStructuredMetadataTableDrawer>
);
};
@@ -0,0 +1,68 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { screen } from '@testing-library/react';
import * as onePersistentVolumeClaimsFixture from '../../__fixtures__/1-persistentvolumeclaims.json';
import * as twoPersistentVolumeClaimsFixture from '../../__fixtures__/2-persistentvolumeclaims.json';
import { renderInTestApp } from '@backstage/test-utils';
import { PersistentVolumeClaimsTable } from './PersistentVolumeClaimsTable';
describe('PersistentVolumeClaimsTable', () => {
it('should render persistent volume claim table with columns', async () => {
await renderInTestApp(
<PersistentVolumeClaimsTable
persistentVolumeClaims={
(onePersistentVolumeClaimsFixture as any).persistentVolumeClaims
}
/>,
);
expect(screen.getByText('name')).toBeInTheDocument();
expect(screen.getByText('phase')).toBeInTheDocument();
expect(screen.getByText('status')).toBeInTheDocument();
expect(screen.getByText('capacity')).toBeInTheDocument();
expect(screen.getByText('volume')).toBeInTheDocument();
expect(screen.getByText('pvc-web-storage')).toBeInTheDocument();
expect(screen.getAllByText('Bound').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('10Gi')).toBeInTheDocument();
});
it('should render multiple persistent volume claims', async () => {
await renderInTestApp(
<PersistentVolumeClaimsTable
persistentVolumeClaims={
(twoPersistentVolumeClaimsFixture as any).persistentVolumeClaims
}
/>,
);
expect(screen.getByText('name')).toBeInTheDocument();
expect(screen.getByText('phase')).toBeInTheDocument();
expect(screen.getByText('status')).toBeInTheDocument();
expect(screen.getByText('capacity')).toBeInTheDocument();
expect(screen.getByText('volume')).toBeInTheDocument();
expect(screen.getByText('pvc-web-storage')).toBeInTheDocument();
expect(screen.getByText('pvc-database-storage')).toBeInTheDocument();
expect(screen.getAllByText('Bound').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('Pending').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('10Gi')).toBeInTheDocument();
expect(screen.getByText('50Gi')).toBeInTheDocument();
});
});
@@ -0,0 +1,136 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ReactNode } from 'react';
import { PersistentVolumeClaimsDrawer } from './PersistentVolumeClaimsDrawer';
import { Table, TableColumn } from '@backstage/core-components';
import type { V1PersistentVolumeClaim } from '@kubernetes/client-node';
import {
StatusError,
StatusOK,
StatusPending,
} from '@backstage/core-components';
export type PersistentVolumeClaimsTableProps = {
persistentVolumeClaims: V1PersistentVolumeClaim[];
children?: ReactNode;
};
const PersistentVolumeClaimDrawerTrigger = ({
persistentVolumeClaim,
}: {
persistentVolumeClaim: V1PersistentVolumeClaim;
}) => {
return (
<PersistentVolumeClaimsDrawer
persistentVolumeClaim={persistentVolumeClaim}
/>
);
};
const renderPhaseStatus = (persistentVolumeClaim: V1PersistentVolumeClaim) => {
const phase = persistentVolumeClaim.status?.phase;
if (phase === 'Bound') {
return <StatusOK>Bound</StatusOK>;
}
if (phase === 'Pending') {
return <StatusPending>Pending</StatusPending>;
}
if (phase === 'Lost') {
return <StatusError>Lost</StatusError>;
}
return <>{phase ?? 'Unknown'}</>;
};
export const PersistentVolumeClaimsTable = ({
persistentVolumeClaims,
}: PersistentVolumeClaimsTableProps) => {
const defaultColumns: TableColumn<V1PersistentVolumeClaim>[] = [
{
title: 'ID',
field: 'metadata.uid',
hidden: true,
},
{
title: 'name',
highlight: true,
render: (persistentVolumeClaim: V1PersistentVolumeClaim) => {
return (
<PersistentVolumeClaimDrawerTrigger
persistentVolumeClaim={persistentVolumeClaim}
/>
);
},
cellStyle: {
width: '25%',
},
},
{
title: 'phase',
render: renderPhaseStatus,
cellStyle: {
width: '10%',
},
},
{
title: 'status',
render: (persistentVolumeClaim: V1PersistentVolumeClaim) =>
persistentVolumeClaim.status?.phase ?? 'Unknown',
cellStyle: {
width: '10%',
},
},
{
title: 'capacity',
render: (persistentVolumeClaim: V1PersistentVolumeClaim) =>
persistentVolumeClaim.status?.capacity?.storage ?? 'N/A',
cellStyle: {
width: '10%',
},
},
{
title: 'volume',
render: (persistentVolumeClaim: V1PersistentVolumeClaim) =>
persistentVolumeClaim.spec?.volumeName ?? 'N/A',
cellStyle: {
width: '45%',
},
},
];
const columns: TableColumn<V1PersistentVolumeClaim>[] = [...defaultColumns];
const tableStyle = {
minWidth: '0',
width: '100%',
};
return (
<div style={tableStyle}>
<Table
options={{ paging: true, search: false, emptyRowsWhenPaging: false }}
data={
persistentVolumeClaims.map((pvc: V1PersistentVolumeClaim) => ({
...pvc,
id: pvc?.metadata?.uid,
})) as any as V1PersistentVolumeClaim[]
}
columns={columns}
/>
</div>
);
};
@@ -0,0 +1,18 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './PersistentVolumeClaimsAccordions';
export * from './PersistentVolumeClaimsTable';
export * from './PersistentVolumeClaimsDrawer';
@@ -35,4 +35,6 @@ export const GroupedResponsesContext = createContext<GroupedResponses>({
cronJobs: [],
customResources: [],
statefulsets: [],
persistentVolumes: [],
persistentVolumeClaims: [],
});
@@ -0,0 +1,83 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getPersistentVolumeType } from './persistentVolumes';
describe('getPersistentVolumeType', () => {
it('should return null for undefined driver', () => {
expect(getPersistentVolumeType(undefined)).toBeNull();
});
it('should return null for empty driver', () => {
expect(getPersistentVolumeType('')).toBeNull();
});
describe('AWS drivers', () => {
it('should return AWS EBS Volume for aws ebs driver', () => {
expect(getPersistentVolumeType('ebs.csi.aws.com')).toBe('AWS EBS Volume');
});
it('should return AWS EFS for aws efs driver', () => {
expect(getPersistentVolumeType('efs.csi.aws.com')).toBe('AWS EFS');
});
it('should return S3 Bucket for aws s3 driver', () => {
expect(getPersistentVolumeType('s3.csi.aws.com')).toBe('S3 Bucket');
});
});
describe('GCP drivers', () => {
it('should return GCP Persistent Disk for gcp pd driver', () => {
expect(getPersistentVolumeType('pd.csi.storage.gke.io')).toBe(
'GCP Persistent Disk',
);
});
it('should return GCP Filestore for gcp filestore driver', () => {
expect(getPersistentVolumeType('filestore.csi.storage.gke.io')).toBe(
'GCP Filestore',
);
});
it('should return GCS Fuse for gcp gcsfuse driver', () => {
expect(getPersistentVolumeType('gcsfuse.csi.storage.gke.io')).toBe(
'GCS Fuse',
);
});
});
describe('Azure drivers', () => {
it('should return Azure Disk for azure disk driver', () => {
expect(getPersistentVolumeType('disk.csi.azure.com')).toBe('Azure Disk');
});
it('should return Azure File for azure file driver', () => {
expect(getPersistentVolumeType('file.csi.azure.com')).toBe('Azure File');
});
it('should return Azure Blob for azure blob driver', () => {
expect(getPersistentVolumeType('blob.csi.azure.com')).toBe('Azure Blob');
});
});
it('should return the original driver for unknown drivers', () => {
expect(getPersistentVolumeType('unknown.driver.com')).toBe(
'unknown.driver.com',
);
expect(getPersistentVolumeType('local-storage')).toBe('local-storage');
expect(getPersistentVolumeType('nfs')).toBe('nfs');
});
});
@@ -0,0 +1,39 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const getPersistentVolumeType = (driver?: string): string | null => {
if (!driver) return null;
if (driver.includes('aws')) {
if (driver.includes('ebs')) return `AWS EBS Volume`;
if (driver.includes('efs')) return `AWS EFS`;
if (driver.includes('s3')) return `S3 Bucket`;
}
if (driver.includes('gke')) {
if (driver.includes('gcsfuse')) return `GCS Fuse`;
if (driver.includes('filestore')) return `GCP Filestore`;
if (driver.includes('pd')) return `GCP Persistent Disk`;
}
if (driver.includes('azure')) {
if (driver.includes('disk')) return `Azure Disk`;
if (driver.includes('file')) return `Azure File`;
if (driver.includes('blob')) return `Azure Blob`;
}
return driver;
};