From 89662103a8c9fcae0b517a98dfb65525a2f4b9ba Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Mon, 26 Jul 2021 09:33:32 +0300 Subject: [PATCH] Fix workload overview status sorting (#3486) * Sort statuses allowing 'running' to be first Signed-off-by: Alex Andreev * Fix Chart legend badges Signed-off-by: Alex Andreev * Adding getStatus() tests for each store Signed-off-by: Alex Andreev --- src/renderer/api/kube-json-api.ts | 1 + .../+workloads-cronjobs/cronjob.store.ts | 2 +- .../+workloads-daemonsets/daemonsets.store.ts | 2 +- .../deployments.store.ts | 2 +- .../components/+workloads-jobs/job.store.ts | 2 +- .../components/+workloads-pods/pods.store.ts | 2 +- .../replicasets.store.ts | 2 +- .../statefulset.store.ts | 2 +- .../__tests__/cronjob.store.test.ts | 115 ++++++++ .../__tests__/daemonset.store.test.ts | 181 ++++++++++++ .../__tests__/deployments.store.test.ts | 265 ++++++++++++++++++ .../components/__tests__/job.store.test.ts | 228 +++++++++++++++ .../components/__tests__/pods.store.test.ts | 159 +++++++++++ .../__tests__/replicaset.store.test.ts | 181 ++++++++++++ .../__tests__/statefulset.store.test.ts | 182 ++++++++++++ .../components/badge/badge.module.css | 4 - src/renderer/components/chart/chart.tsx | 6 +- 17 files changed, 1322 insertions(+), 14 deletions(-) create mode 100644 src/renderer/components/__tests__/cronjob.store.test.ts create mode 100644 src/renderer/components/__tests__/daemonset.store.test.ts create mode 100644 src/renderer/components/__tests__/deployments.store.test.ts create mode 100644 src/renderer/components/__tests__/job.store.test.ts create mode 100644 src/renderer/components/__tests__/pods.store.test.ts create mode 100644 src/renderer/components/__tests__/replicaset.store.test.ts create mode 100644 src/renderer/components/__tests__/statefulset.store.test.ts diff --git a/src/renderer/api/kube-json-api.ts b/src/renderer/api/kube-json-api.ts index 362494a659..ebb01b696c 100644 --- a/src/renderer/api/kube-json-api.ts +++ b/src/renderer/api/kube-json-api.ts @@ -48,6 +48,7 @@ export interface KubeJsonApiMetadata { annotations?: { [annotation: string]: string; }; + [key: string]: any; } export interface KubeJsonApiData extends JsonApiData { diff --git a/src/renderer/components/+workloads-cronjobs/cronjob.store.ts b/src/renderer/components/+workloads-cronjobs/cronjob.store.ts index 422cc7b69d..90ea0e0c2c 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjob.store.ts +++ b/src/renderer/components/+workloads-cronjobs/cronjob.store.ts @@ -34,7 +34,7 @@ export class CronJobStore extends KubeObjectStore { } getStatuses(cronJobs?: CronJob[]) { - const status = { suspended: 0, scheduled: 0 }; + const status = { scheduled: 0, suspended: 0 }; cronJobs.forEach(cronJob => { if (cronJob.spec.suspend) { diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts b/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts index 095eb2b9d7..e8f729fde1 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts +++ b/src/renderer/components/+workloads-daemonsets/daemonsets.store.ts @@ -41,7 +41,7 @@ export class DaemonSetStore extends KubeObjectStore { } getStatuses(daemonSets?: DaemonSet[]) { - const status = { failed: 0, pending: 0, running: 0 }; + const status = { running: 0, failed: 0, pending: 0 }; daemonSets.forEach(daemonSet => { const pods = this.getChildPods(daemonSet); diff --git a/src/renderer/components/+workloads-deployments/deployments.store.ts b/src/renderer/components/+workloads-deployments/deployments.store.ts index 96149c8745..34b6e295ad 100644 --- a/src/renderer/components/+workloads-deployments/deployments.store.ts +++ b/src/renderer/components/+workloads-deployments/deployments.store.ts @@ -43,7 +43,7 @@ export class DeploymentStore extends KubeObjectStore { } getStatuses(deployments?: Deployment[]) { - const status = { failed: 0, pending: 0, running: 0 }; + const status = { running: 0, failed: 0, pending: 0 }; deployments.forEach(deployment => { const pods = this.getChildPods(deployment); diff --git a/src/renderer/components/+workloads-jobs/job.store.ts b/src/renderer/components/+workloads-jobs/job.store.ts index 754c8562c8..643f29385a 100644 --- a/src/renderer/components/+workloads-jobs/job.store.ts +++ b/src/renderer/components/+workloads-jobs/job.store.ts @@ -46,7 +46,7 @@ export class JobStore extends KubeObjectStore { } getStatuses(jobs?: Job[]) { - const status = { failed: 0, pending: 0, running: 0, succeeded: 0 }; + const status = { succeeded: 0, running: 0, failed: 0, pending: 0 }; jobs.forEach(job => { const pods = this.getChildPods(job); diff --git a/src/renderer/components/+workloads-pods/pods.store.ts b/src/renderer/components/+workloads-pods/pods.store.ts index 1708690a8f..9c2b925400 100644 --- a/src/renderer/components/+workloads-pods/pods.store.ts +++ b/src/renderer/components/+workloads-pods/pods.store.ts @@ -70,7 +70,7 @@ export class PodsStore extends KubeObjectStore { } getStatuses(pods: Pod[]) { - return countBy(pods.map(pod => pod.getStatus())); + return countBy(pods.map(pod => pod.getStatus()).sort().reverse()); } getPodKubeMetrics(pod: Pod) { diff --git a/src/renderer/components/+workloads-replicasets/replicasets.store.ts b/src/renderer/components/+workloads-replicasets/replicasets.store.ts index c7d7729889..5daa57e45d 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.store.ts +++ b/src/renderer/components/+workloads-replicasets/replicasets.store.ts @@ -42,7 +42,7 @@ export class ReplicaSetStore extends KubeObjectStore { } getStatuses(replicaSets: ReplicaSet[]) { - const status = { failed: 0, pending: 0, running: 0 }; + const status = { running: 0, failed: 0, pending: 0 }; replicaSets.forEach(replicaSet => { const pods = this.getChildPods(replicaSet); diff --git a/src/renderer/components/+workloads-statefulsets/statefulset.store.ts b/src/renderer/components/+workloads-statefulsets/statefulset.store.ts index c1908c01a5..73c03a1b60 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulset.store.ts +++ b/src/renderer/components/+workloads-statefulsets/statefulset.store.ts @@ -41,7 +41,7 @@ export class StatefulSetStore extends KubeObjectStore { } getStatuses(statefulSets: StatefulSet[]) { - const status = { failed: 0, pending: 0, running: 0 }; + const status = { running: 0, failed: 0, pending: 0 }; statefulSets.forEach(statefulSet => { const pods = this.getChildPods(statefulSet); diff --git a/src/renderer/components/__tests__/cronjob.store.test.ts b/src/renderer/components/__tests__/cronjob.store.test.ts new file mode 100644 index 0000000000..fcd8f93023 --- /dev/null +++ b/src/renderer/components/__tests__/cronjob.store.test.ts @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; +import { CronJob } from "../../api/endpoints"; + +const spec = { + schedule: "test", + concurrencyPolicy: "test", + suspend: true, + jobTemplate: { + metadata: {}, + spec: { + template: { + metadata: {}, + spec: { + containers: [] as any, + restartPolicy: "restart", + terminationGracePeriodSeconds: 1, + dnsPolicy: "no", + hostPID: true, + schedulerName: "string" + } + } + } + }, + successfulJobsHistoryLimit: 1, + failedJobsHistoryLimit: 1 +}; + +const scheduledCronJob = new CronJob({ + apiVersion: "foo", + kind: "CronJob", + metadata: { + name: "scheduledCronJob", + resourceVersion: "scheduledCronJob", + uid: "scheduledCronJob", + namespace: "default", + }, +}); + +const suspendedCronJob = new CronJob({ + apiVersion: "foo", + kind: "CronJob", + metadata: { + name: "suspendedCronJob", + resourceVersion: "suspendedCronJob", + uid: "suspendedCronJob", + namespace: "default", + } +}); + +const otherSuspendedCronJob = new CronJob({ + apiVersion: "foo", + kind: "CronJob", + metadata: { + name: "otherSuspendedCronJob", + resourceVersion: "otherSuspendedCronJob", + uid: "otherSuspendedCronJob", + namespace: "default", + }, +}); + +scheduledCronJob.spec = { ...spec }; +suspendedCronJob.spec = { ...spec }; +otherSuspendedCronJob.spec = { ...spec }; +scheduledCronJob.spec.suspend = false; + +describe("CronJob Store tests", () => { + it("gets CronJob statuses in proper sorting order", () => { + const statuses = Object.entries(cronJobStore.getStatuses([ + suspendedCronJob, + otherSuspendedCronJob, + scheduledCronJob + ])); + + expect(statuses).toEqual([ + ["scheduled", 1], + ["suspended", 2], + ]); + }); + + it("returns 0 for other statuses", () => { + let statuses = Object.entries(cronJobStore.getStatuses([scheduledCronJob])); + + expect(statuses).toEqual([ + ["scheduled", 1], + ["suspended", 0], + ]); + + statuses = Object.entries(cronJobStore.getStatuses([suspendedCronJob])); + + expect(statuses).toEqual([ + ["scheduled", 0], + ["suspended", 1], + ]); + }); +}); diff --git a/src/renderer/components/__tests__/daemonset.store.test.ts b/src/renderer/components/__tests__/daemonset.store.test.ts new file mode 100644 index 0000000000..00dcce5468 --- /dev/null +++ b/src/renderer/components/__tests__/daemonset.store.test.ts @@ -0,0 +1,181 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { observable } from "mobx"; +import { daemonSetStore } from "../+workloads-daemonsets/daemonsets.store"; +import { podsStore } from "../+workloads-pods/pods.store"; +import { DaemonSet, Pod } from "../../api/endpoints"; + +const runningDaemonSet = new DaemonSet({ + apiVersion: "foo", + kind: "DaemonSet", + metadata: { + name: "runningDaemonSet", + resourceVersion: "runningDaemonSet", + uid: "runningDaemonSet", + namespace: "default", + }, +}); + +const failedDaemonSet = new DaemonSet({ + apiVersion: "foo", + kind: "DaemonSet", + metadata: { + name: "failedDaemonSet", + resourceVersion: "failedDaemonSet", + uid: "failedDaemonSet", + namespace: "default", + }, +}); + +const pendingDaemonSet = new DaemonSet({ + apiVersion: "foo", + kind: "DaemonSet", + metadata: { + name: "pendingDaemonSet", + resourceVersion: "pendingDaemonSet", + uid: "pendingDaemonSet", + namespace: "default", + }, +}); + +const runningPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar", + resourceVersion: "foobar", + uid: "foobar", + ownerReferences: [{ + uid: "runningDaemonSet", + }], + namespace: "default" + }, +}); + +runningPod.status = { + phase: "Running", + conditions: [ + { + type: "Initialized", + status: "True", + lastProbeTime: 1, + lastTransitionTime: "1", + }, + { + type: "Ready", + status: "True", + lastProbeTime: 1, + lastTransitionTime: "1", + } + ], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", + containerStatuses: [], + initContainerStatuses: [], +}; + +const pendingPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-pending", + resourceVersion: "foobar", + uid: "foobar-pending", + ownerReferences: [{ + uid: "pendingDaemonSet", + }], + namespace: "default" + }, +}); + +const failedPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-failed", + resourceVersion: "foobar", + uid: "foobar-failed", + ownerReferences: [{ + uid: "failedDaemonSet", + }], + namespace: "default" + }, +}); + +failedPod.status = { + phase: "Failed", + conditions: [], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", +}; + +describe("DaemonSet Store tests", () => { + beforeAll(() => { + podsStore.items = observable.array([ + runningPod, + failedPod, + pendingPod + ]); + }); + + it("gets DaemonSet statuses in proper sorting order", () => { + const statuses = Object.entries(daemonSetStore.getStatuses([ + failedDaemonSet, + runningDaemonSet, + pendingDaemonSet + ])); + + expect(statuses).toEqual([ + ["running", 1], + ["failed", 1], + ["pending", 1], + ]); + }); + + it("returns 0 for other statuses", () => { + let statuses = Object.entries(daemonSetStore.getStatuses([runningDaemonSet])); + + expect(statuses).toEqual([ + ["running", 1], + ["failed", 0], + ["pending", 0], + ]); + + statuses = Object.entries(daemonSetStore.getStatuses([failedDaemonSet])); + + expect(statuses).toEqual([ + ["running", 0], + ["failed", 1], + ["pending", 0], + ]); + + statuses = Object.entries(daemonSetStore.getStatuses([pendingDaemonSet])); + + expect(statuses).toEqual([ + ["running", 0], + ["failed", 0], + ["pending", 1], + ]); + }); +}); diff --git a/src/renderer/components/__tests__/deployments.store.test.ts b/src/renderer/components/__tests__/deployments.store.test.ts new file mode 100644 index 0000000000..6d7552dd6b --- /dev/null +++ b/src/renderer/components/__tests__/deployments.store.test.ts @@ -0,0 +1,265 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { observable } from "mobx"; +import { deploymentStore } from "../+workloads-deployments/deployments.store"; +import { podsStore } from "../+workloads-pods/pods.store"; +import { Deployment, Pod } from "../../api/endpoints"; + +const spec = { + containers: [{ + name: "some", + image: "someimage", + resources: { + requests: { + cpu: "2", + memory: "2Gi" + }, + }, + terminationMessagePath: "test", + terminationMessagePolicy: "test", + imagePullPolicy: "test", + }], + restartPolicy: "restart", + terminationGracePeriodSeconds: 1200, + dnsPolicy: "dns", + serviceAccountName: "test", + serviceAccount: "test", + securityContext: {}, + schedulerName: "test" +}; + +const runningDeployment = new Deployment({ + apiVersion: "foo", + kind: "Deployment", + metadata: { + name: "foobar", + resourceVersion: "foobar", + uid: "foobar", + namespace: "default", + }, +}); + +runningDeployment.spec = { + replicas: 1, + selector: { matchLabels: {} }, + strategy: { + type: "test", + rollingUpdate: { + maxSurge: 1, + maxUnavailable: 1 + } + }, + template: { + metadata: { + labels: { + "name": "kube-state-metrics" + } + }, + spec + } +}; + +const failedDeployment = new Deployment({ + apiVersion: "foo", + kind: "Deployment", + metadata: { + name: "failedDeployment", + resourceVersion: "failedDeployment", + uid: "failedDeployment", + namespace: "default", + }, +}); + +failedDeployment.spec = { + replicas: 1, + selector: { matchLabels: {} }, + strategy: { + type: "test", + rollingUpdate: { + maxSurge: 1, + maxUnavailable: 1 + } + }, + template: { + metadata: { + labels: { + "name": "failedpods" + } + }, + spec + } +}; + +const pendingDeployment = new Deployment({ + apiVersion: "foo", + kind: "Deployment", + metadata: { + name: "pendingDeployment", + resourceVersion: "pendingDeployment", + uid: "pendingDeployment", + namespace: "default", + }, +}); + +pendingDeployment.spec = { + replicas: 1, + selector: { matchLabels: {} }, + strategy: { + type: "test", + rollingUpdate: { + maxSurge: 1, + maxUnavailable: 1 + } + }, + template: { + metadata: { + labels: { + "mydeployment": "true" + } + }, + spec + } +}; + +const runningPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar", + resourceVersion: "foobar", + uid: "foobar", + labels: { + "name": "kube-state-metrics" + }, + namespace: "default" + }, +}); + +runningPod.status = { + phase: "Running", + conditions: [ + { + type: "Initialized", + status: "True", + lastProbeTime: 1, + lastTransitionTime: "1", + }, + { + type: "Ready", + status: "True", + lastProbeTime: 1, + lastTransitionTime: "1", + } + ], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", + containerStatuses: [], + initContainerStatuses: [], +}; + +const pendingPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-pending", + resourceVersion: "foobar", + uid: "foobar-pending", + labels: { + "mydeployment": "true" + }, + namespace: "default" + }, +}); + +const failedPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-failed", + resourceVersion: "foobar", + uid: "foobar-failed", + labels: { + "name": "failedpods" + }, + namespace: "default" + }, +}); + +failedPod.status = { + phase: "Failed", + conditions: [], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", +}; + +describe("Deployment Store tests", () => { + beforeAll(() => { + // Add pods to pod store + podsStore.items = observable.array([ + runningPod, + failedPod, + pendingPod + ]); + }); + + it("gets Deployment statuses in proper sorting order", () => { + const statuses = Object.entries(deploymentStore.getStatuses([ + failedDeployment, + runningDeployment, + pendingDeployment + ])); + + expect(statuses).toEqual([ + ["running", 1], + ["failed", 1], + ["pending", 1], + ]); + }); + + it("returns 0 for other statuses", () => { + let statuses = Object.entries(deploymentStore.getStatuses([runningDeployment])); + + expect(statuses).toEqual([ + ["running", 1], + ["failed", 0], + ["pending", 0], + ]); + + statuses = Object.entries(deploymentStore.getStatuses([failedDeployment])); + + expect(statuses).toEqual([ + ["running", 0], + ["failed", 1], + ["pending", 0], + ]); + + statuses = Object.entries(deploymentStore.getStatuses([pendingDeployment])); + + expect(statuses).toEqual([ + ["running", 0], + ["failed", 0], + ["pending", 1], + ]); + }); +}); diff --git a/src/renderer/components/__tests__/job.store.test.ts b/src/renderer/components/__tests__/job.store.test.ts new file mode 100644 index 0000000000..9f3a81ba91 --- /dev/null +++ b/src/renderer/components/__tests__/job.store.test.ts @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { observable } from "mobx"; +import { jobStore } from "../+workloads-jobs/job.store"; +import { podsStore } from "../+workloads-pods/pods.store"; +import { Job, Pod } from "../../api/endpoints"; + +const runningJob = new Job({ + apiVersion: "foo", + kind: "Job", + metadata: { + name: "runningJob", + resourceVersion: "runningJob", + uid: "runningJob", + namespace: "default", + }, +}); + +const failedJob = new Job({ + apiVersion: "foo", + kind: "Job", + metadata: { + name: "failedJob", + resourceVersion: "failedJob", + uid: "failedJob", + namespace: "default", + }, +}); + +const pendingJob = new Job({ + apiVersion: "foo", + kind: "Job", + metadata: { + name: "pendingJob", + resourceVersion: "pendingJob", + uid: "pendingJob", + namespace: "default", + }, +}); + +const succeededJob = new Job({ + apiVersion: "foo", + kind: "Job", + metadata: { + name: "succeededJob", + resourceVersion: "succeededJob", + uid: "succeededJob", + namespace: "default", + }, +}); + +const runningPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar", + resourceVersion: "foobar", + uid: "foobar", + ownerReferences: [{ + uid: "runningJob", + }], + namespace: "default" + }, +}); + +runningPod.status = { + phase: "Running", + conditions: [ + { + type: "Initialized", + status: "True", + lastProbeTime: 1, + lastTransitionTime: "1", + }, + { + type: "Ready", + status: "True", + lastProbeTime: 1, + lastTransitionTime: "1", + } + ], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", + containerStatuses: [], + initContainerStatuses: [], +}; + +const pendingPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-pending", + resourceVersion: "foobar", + uid: "foobar-pending", + ownerReferences: [{ + uid: "pendingJob", + }], + namespace: "default" + }, +}); + +const failedPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-failed", + resourceVersion: "foobar", + uid: "foobar-failed", + ownerReferences: [{ + uid: "failedJob", + }], + namespace: "default" + }, +}); + +failedPod.status = { + phase: "Failed", + conditions: [], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", +}; + +const succeededPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-succeeded", + resourceVersion: "foobar", + uid: "foobar-succeeded", + ownerReferences: [{ + uid: "succeededJob", + }], + }, +}); + +succeededPod.status = { + phase: "Succeeded", + conditions: [], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", +}; + +describe("Job Store tests", () => { + beforeAll(() => { + podsStore.items = observable.array([ + runningPod, + failedPod, + pendingPod, + succeededPod + ]); + }); + + it("gets Job statuses in proper sorting order", () => { + const statuses = Object.entries(jobStore.getStatuses([ + failedJob, + succeededJob, + runningJob, + pendingJob + ])); + + expect(statuses).toEqual([ + ["succeeded", 1], + ["running", 1], + ["failed", 1], + ["pending", 1], + ]); + }); + + it("returns 0 for other statuses", () => { + let statuses = Object.entries(jobStore.getStatuses([succeededJob])); + + expect(statuses).toEqual([ + ["succeeded", 1], + ["running", 0], + ["failed", 0], + ["pending", 0], + ]); + + statuses = Object.entries(jobStore.getStatuses([runningJob])); + + expect(statuses).toEqual([ + ["succeeded", 0], + ["running", 1], + ["failed", 0], + ["pending", 0], + ]); + + statuses = Object.entries(jobStore.getStatuses([failedJob])); + + expect(statuses).toEqual([ + ["succeeded", 0], + ["running", 0], + ["failed", 1], + ["pending", 0], + ]); + + statuses = Object.entries(jobStore.getStatuses([pendingJob])); + + expect(statuses).toEqual([ + ["succeeded", 0], + ["running", 0], + ["failed", 0], + ["pending", 1], + ]); + }); +}); diff --git a/src/renderer/components/__tests__/pods.store.test.ts b/src/renderer/components/__tests__/pods.store.test.ts new file mode 100644 index 0000000000..69d4f49840 --- /dev/null +++ b/src/renderer/components/__tests__/pods.store.test.ts @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { Pod } from "../../api/endpoints"; +import { podsStore } from "../+workloads-pods/pods.store"; + +const runningPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar", + resourceVersion: "foobar", + uid: "foobar", + }, +}); + +runningPod.status = { + phase: "Running", + conditions: [ + { + type: "Initialized", + status: "True", + lastProbeTime: 1, + lastTransitionTime: "1", + }, + { + type: "Ready", + status: "True", + lastProbeTime: 1, + lastTransitionTime: "1", + } + ], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", + containerStatuses: [], + initContainerStatuses: [], +}; + +const pendingPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-pending", + resourceVersion: "foobar", + uid: "foobar-pending", + }, +}); + +const failedPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-failed", + resourceVersion: "foobar", + uid: "foobar-failed", + }, +}); + +failedPod.status = { + phase: "Failed", + conditions: [], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", +}; + +const evictedPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-evicted", + resourceVersion: "foobar", + uid: "foobar-evicted", + }, +}); + +evictedPod.status = { + phase: "Failed", + reason: "Evicted", + conditions: [], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", +}; + +const succeededPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-succeeded", + resourceVersion: "foobar", + uid: "foobar-succeeded", + }, +}); + +succeededPod.status = { + phase: "Succeeded", + conditions: [], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", +}; + +describe("Pod Store tests", () => { + it("gets Pod statuses in proper sorting order", () => { + const statuses = Object.entries(podsStore.getStatuses([ + pendingPod, + runningPod, + succeededPod, + failedPod, + evictedPod, + evictedPod + ])); + + expect(statuses).toEqual([ + ["Succeeded", 1], + ["Running", 1], + ["Pending", 1], + ["Failed", 1], + ["Evicted", 2], + ]); + }); + + it("counts statuses properly", () => { + const statuses = Object.entries(podsStore.getStatuses([ + pendingPod, + pendingPod, + pendingPod, + runningPod, + failedPod, + failedPod, + ])); + + expect(statuses).toEqual([ + ["Running", 1], + ["Pending", 3], + ["Failed", 2], + ]); + }); +}); diff --git a/src/renderer/components/__tests__/replicaset.store.test.ts b/src/renderer/components/__tests__/replicaset.store.test.ts new file mode 100644 index 0000000000..74404e9004 --- /dev/null +++ b/src/renderer/components/__tests__/replicaset.store.test.ts @@ -0,0 +1,181 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { observable } from "mobx"; +import { podsStore } from "../+workloads-pods/pods.store"; +import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; +import { ReplicaSet, Pod } from "../../api/endpoints"; + +const runningReplicaSet = new ReplicaSet({ + apiVersion: "foo", + kind: "ReplicaSet", + metadata: { + name: "runningReplicaSet", + resourceVersion: "runningReplicaSet", + uid: "runningReplicaSet", + namespace: "default", + }, +}); + +const failedReplicaSet = new ReplicaSet({ + apiVersion: "foo", + kind: "ReplicaSet", + metadata: { + name: "failedReplicaSet", + resourceVersion: "failedReplicaSet", + uid: "failedReplicaSet", + namespace: "default", + }, +}); + +const pendingReplicaSet = new ReplicaSet({ + apiVersion: "foo", + kind: "ReplicaSet", + metadata: { + name: "pendingReplicaSet", + resourceVersion: "pendingReplicaSet", + uid: "pendingReplicaSet", + namespace: "default", + }, +}); + +const runningPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar", + resourceVersion: "foobar", + uid: "foobar", + ownerReferences: [{ + uid: "runningReplicaSet", + }], + namespace: "default" + }, +}); + +runningPod.status = { + phase: "Running", + conditions: [ + { + type: "Initialized", + status: "True", + lastProbeTime: 1, + lastTransitionTime: "1", + }, + { + type: "Ready", + status: "True", + lastProbeTime: 1, + lastTransitionTime: "1", + } + ], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", + containerStatuses: [], + initContainerStatuses: [], +}; + +const pendingPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-pending", + resourceVersion: "foobar", + uid: "foobar-pending", + ownerReferences: [{ + uid: "pendingReplicaSet", + }], + namespace: "default" + }, +}); + +const failedPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-failed", + resourceVersion: "foobar", + uid: "foobar-failed", + ownerReferences: [{ + uid: "failedReplicaSet", + }], + namespace: "default" + }, +}); + +failedPod.status = { + phase: "Failed", + conditions: [], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", +}; + +describe("ReplicaSet Store tests", () => { + beforeAll(() => { + podsStore.items = observable.array([ + runningPod, + failedPod, + pendingPod + ]); + }); + + it("gets ReplicaSet statuses in proper sorting order", () => { + const statuses = Object.entries(replicaSetStore.getStatuses([ + failedReplicaSet, + runningReplicaSet, + pendingReplicaSet + ])); + + expect(statuses).toEqual([ + ["running", 1], + ["failed", 1], + ["pending", 1], + ]); + }); + + it("returns 0 for other statuses", () => { + let statuses = Object.entries(replicaSetStore.getStatuses([runningReplicaSet])); + + expect(statuses).toEqual([ + ["running", 1], + ["failed", 0], + ["pending", 0], + ]); + + statuses = Object.entries(replicaSetStore.getStatuses([failedReplicaSet])); + + expect(statuses).toEqual([ + ["running", 0], + ["failed", 1], + ["pending", 0], + ]); + + statuses = Object.entries(replicaSetStore.getStatuses([pendingReplicaSet])); + + expect(statuses).toEqual([ + ["running", 0], + ["failed", 0], + ["pending", 1], + ]); + }); +}); diff --git a/src/renderer/components/__tests__/statefulset.store.test.ts b/src/renderer/components/__tests__/statefulset.store.test.ts new file mode 100644 index 0000000000..162684f092 --- /dev/null +++ b/src/renderer/components/__tests__/statefulset.store.test.ts @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { observable } from "mobx"; +import { podsStore } from "../+workloads-pods/pods.store"; +import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store"; +import { StatefulSet, Pod } from "../../api/endpoints"; + +const runningStatefulSet = new StatefulSet({ + apiVersion: "foo", + kind: "StatefulSet", + metadata: { + name: "runningStatefulSet", + resourceVersion: "runningStatefulSet", + uid: "runningStatefulSet", + namespace: "default", + }, +}); + +const failedStatefulSet = new StatefulSet({ + apiVersion: "foo", + kind: "StatefulSet", + metadata: { + name: "failedStatefulSet", + resourceVersion: "failedStatefulSet", + uid: "failedStatefulSet", + namespace: "default", + }, +}); + +const pendingStatefulSet = new StatefulSet({ + apiVersion: "foo", + kind: "StatefulSet", + metadata: { + name: "pendingStatefulSet", + resourceVersion: "pendingStatefulSet", + uid: "pendingStatefulSet", + namespace: "default", + }, +}); + +const runningPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar", + resourceVersion: "foobar", + uid: "foobar", + ownerReferences: [{ + uid: "runningStatefulSet", + }], + namespace: "default" + }, +}); + +runningPod.status = { + phase: "Running", + conditions: [ + { + type: "Initialized", + status: "True", + lastProbeTime: 1, + lastTransitionTime: "1", + }, + { + type: "Ready", + status: "True", + lastProbeTime: 1, + lastTransitionTime: "1", + } + ], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", + containerStatuses: [], + initContainerStatuses: [], +}; + +const pendingPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-pending", + resourceVersion: "foobar", + uid: "foobar-pending", + ownerReferences: [{ + uid: "pendingStatefulSet", + }], + namespace: "default" + }, +}); + +const failedPod = new Pod({ + apiVersion: "foo", + kind: "Pod", + metadata: { + name: "foobar-failed", + resourceVersion: "foobar", + uid: "foobar-failed", + ownerReferences: [{ + uid: "failedStatefulSet", + }], + namespace: "default" + }, +}); + +failedPod.status = { + phase: "Failed", + conditions: [], + hostIP: "10.0.0.1", + podIP: "10.0.0.1", + startTime: "now", +}; + +describe("StatefulSet Store tests", () => { + beforeAll(() => { + // Add pods to pod store + podsStore.items = observable.array([ + runningPod, + failedPod, + pendingPod + ]); + }); + + it("gets StatefulSet statuses in proper sorting order", () => { + const statuses = Object.entries(statefulSetStore.getStatuses([ + failedStatefulSet, + runningStatefulSet, + pendingStatefulSet + ])); + + expect(statuses).toEqual([ + ["running", 1], + ["failed", 1], + ["pending", 1], + ]); + }); + + it("returns 0 for other statuses", () => { + let statuses = Object.entries(statefulSetStore.getStatuses([runningStatefulSet])); + + expect(statuses).toEqual([ + ["running", 1], + ["failed", 0], + ["pending", 0], + ]); + + statuses = Object.entries(statefulSetStore.getStatuses([failedStatefulSet])); + + expect(statuses).toEqual([ + ["running", 0], + ["failed", 1], + ["pending", 0], + ]); + + statuses = Object.entries(statefulSetStore.getStatuses([pendingStatefulSet])); + + expect(statuses).toEqual([ + ["running", 0], + ["failed", 0], + ["pending", 1], + ]); + }); +}); diff --git a/src/renderer/components/badge/badge.module.css b/src/renderer/components/badge/badge.module.css index 19dc8c7631..8fe7a0a022 100644 --- a/src/renderer/components/badge/badge.module.css +++ b/src/renderer/components/badge/badge.module.css @@ -25,10 +25,6 @@ max-width: 100%; } -.badge + .badge { - margin-left: 8px; -} - .badge.interactive:hover { background-color: var(--mainBackground); cursor: pointer; diff --git a/src/renderer/components/chart/chart.tsx b/src/renderer/components/chart/chart.tsx index d456c397d1..7f40c0e174 100644 --- a/src/renderer/components/chart/chart.tsx +++ b/src/renderer/components/chart/chart.tsx @@ -171,8 +171,8 @@ export class Chart extends React.Component { key={title} className="LegendBadge flex gaps align-center" label={( -
- +
+ {title}
)} @@ -182,7 +182,7 @@ export class Chart extends React.Component { ); return ( -
+
{labels && labels.map((label: string, index) => { const { backgroundColor } = datasets[0] as any; const color = legendColors ? legendColors[index] : backgroundColor[index];