diff --git a/src/common/k8s-api/endpoints/metrics.api.ts b/src/common/k8s-api/endpoints/metrics.api.ts index 88efc166d1..ad4044c5d3 100644 --- a/src/common/k8s-api/endpoints/metrics.api.ts +++ b/src/common/k8s-api/endpoints/metrics.api.ts @@ -141,8 +141,11 @@ export function isMetricsEmpty(metrics: Record) { return Object.values(metrics).every(metric => !metric?.data?.result?.length); } -export function getItemMetrics(metrics: Record, itemName: string): Record | void { - if (!metrics) return; +export function getItemMetrics(metrics: Record, itemName: string): Record | undefined { + if (!metrics) { + return undefined; + } + const itemMetrics = { ...metrics }; for (const metric in metrics) { diff --git a/src/common/k8s-api/endpoints/persistent-volume-claims.api.ts b/src/common/k8s-api/endpoints/persistent-volume-claims.api.ts index a7815e774c..cbe920421d 100644 --- a/src/common/k8s-api/endpoints/persistent-volume-claims.api.ts +++ b/src/common/k8s-api/endpoints/persistent-volume-claims.api.ts @@ -31,17 +31,25 @@ export interface IPvcMetrics { diskCapacity: T; } -export interface PersistentVolumeClaim { - spec: { - accessModes: string[]; - storageClassName: string; - selector: LabelSelector; - resources: { - requests: { - storage: string; // 8Gi - }; - }; +export interface PersistentVolumeClaimSpec { + accessModes: string[]; + selector: LabelSelector; + resources: { + requests?: Record; + limits?: Record; }; + volumeName?: string; + storageClassName?: string; + volumeMode?: string; + dataSource?: { + apiGroup: string; + kind: string; + name: string; + }; +} + +export interface PersistentVolumeClaim { + spec: PersistentVolumeClaimSpec; status: { phase: string; // Pending }; diff --git a/src/common/k8s-api/endpoints/pods.api.ts b/src/common/k8s-api/endpoints/pods.api.ts index facc1fa3c3..6226deb59e 100644 --- a/src/common/k8s-api/endpoints/pods.api.ts +++ b/src/common/k8s-api/endpoints/pods.api.ts @@ -9,6 +9,10 @@ import { IMetrics, metricsApi } from "./metrics.api"; import { KubeApi } from "../kube-api"; import type { KubeJsonApiData } from "../kube-json-api"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; +import type { RequireExactlyOne } from "type-fest"; +import type { KubeObjectMetadata, LocalObjectReference } from "../kube-object"; +import type { SecretReference } from "./secret.api"; +import type { PersistentVolumeClaimSpec } from "./persistent-volume-claims.api"; export class PodsApi extends KubeApi { getLogs = async (params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise => { @@ -112,12 +116,8 @@ export interface IPodContainer extends Partial; + spec: PersistentVolumeClaimSpec; + }; +} + +export interface EmptyDirSource { + medium?: string; + sizeLimit?: string; +} + +export interface FiberChannelSource { + /** + * A list of World Wide Names + */ + targetWWNs: string[]; + /** + * Logical Unit number + */ + lun: number; + /** + * The type of filesystem + * @default "ext4" + */ + fsType?: string; + readOnly: boolean; +} + +export interface FlokerSource { + datasetName: string; +} + +export interface FlexVolumeSource { + driver: string; + fsType?: string; + secretRef?: LocalObjectReference; + /** + * @default false + */ + readOnly?: boolean; + options?: Record; +} + +export interface GcePersistentDiskSource { + pdName: string; + fsType: string; +} + +export interface GitRepoSource { + repository: string; + revision: string; +} + +export interface GlusterFsSource { + /** + * The name of the Endpoints object that represents a Gluster cluster configuration. + */ + endpoints: string; + /** + * The Glusterfs volume name. + */ + path: string; + /** + * The boolean that sets the mountpoint readOnly or readWrite. + */ + readOnly: boolean; +} + +export interface HostPathSource { + path: string; + /** + * Determines the sorts of checks that will be done + * @default "" + */ + type?: "" | "DirectoryOrCreate" | "Directory" | "FileOrCreate" | "File" | "Socket" | "CharDevice" | "BlockDevice"; +} + +export interface IscsiSource { + targetPortal: string; + iqn: string; + lun: number; + fsType: string; + readOnly: boolean; + chapAuthDiscovery?: boolean; + chapAuthSession?: boolean; + secretRef?: SecretReference; +} + +export interface LocalSource { + path: string; +} + +export interface NetworkFsSource { + server: string; + path: string; + readOnly?: boolean; +} + +export interface PVCSource { + claimName: string; +} + +export interface PhotonPersistentDiskSource { + pdID: string; + /** + * @default "ext4" + */ + fsType?: string; +} + +export interface PortworxVolumeSource { + volumeID: string; + fsType?: string; + readOnly?: boolean; +} + +export interface ProjectedSource { + sources: { + secret?: { + name: string; + items?: { + key: string; + path: string; + mode?: number; + }[]; + }; + downwardAPI?: { + items?: { + path: string; + fieldRef?: { + fieldPath: string; + apiVersion?: string; + }; + resourceFieldRef?: { + resource: string; + containerName?: string; + }; + mode?: number; + }[]; + }; + configMap?: { + name: string; + items?: { + key: string; + path: string; + mode?: number; + }[]; + optional?: boolean; + }; + serviceAccountToken?: { + audience?: string; + expirationSeconds?: number; + path: string; + }; + }[]; + defaultMode: number; +} + +export interface QuobyteSource { + registry: string; + volume: string; + /** + * @default false + */ + readOnly?: boolean; + /** + * @default "serivceaccount" + */ + user?: string; + group?: string; + tenant?: string; +} + +export interface RbdSource { + monitors: string[]; + image: string; + /** + * @default "ext4" + */ + fsType?: string; + /** + * @default "rbd" + */ + pool?: string; + /** + * @default "admin" + */ + user?: string; + /** + * @default "/etc/ceph/keyring" + */ + keyring?: string; + secretRef?: SecretReference; + /** + * @default false + */ + readOnly?: boolean; +} + +export interface ScaleIoSource { + gateway: string; + system: string; + secretRef?: LocalObjectReference; + /** + * @default false + */ + sslEnabled?: boolean; + protectionDomain?: string; + storagePool?: string; + /** + * @default "ThinProvisioned" + */ + storageMode?: "ThickProvisioned" | "ThinProvisioned"; + volumeName: string; + /** + * @default "xfs" + */ + fsType?: string; + /** + * @default false + */ + readOnly?: boolean; +} + +export interface SecretSource { + secretName: string; + items?: { + key: string; + path: string; + mode?: number; + }[]; + defaultMode?: number; + optional?: boolean; +} + +export interface StorageOsSource { + volumeName: string; + /** + * @default Pod.metadata.namespace + */ + volumeNamespace?: string; + /** + * @default "ext4" + */ + fsType?: string; + /** + * @default false + */ + readOnly?: boolean; + secretRef?: LocalObjectReference; +} + +export interface VsphereVolumeSource { + volumePath: string; + /** + * @default "ext4" + */ + fsType?: string; + storagePolicyName?: string; + storagePolicyID?: string; +} + +export interface ContainerStorageInterfaceSource { + driver: string; + /** + * @default false + */ + readOnly?: boolean; + /** + * @default "ext4" + */ + fsType?: string; + volumeAttributes?: Record; + controllerPublishSecretRef?: SecretReference; + nodeStageSecretRef?: SecretReference; + nodePublishSecretRef?: SecretReference; + controllerExpandSecretRef?: SecretReference; +} + +export interface PodVolumeVariants { + awsElasticBlockStore: AwsElasticBlockStoreSource; + azureDisk: AzureDiskSource; + azureFile: AzureFileSource; + cephfs: CephfsSource; + cinder: CinderSource; + configMap: ConfigMapSource; + downwardAPI: DownwardApiSource; + ephemeral: EphemeralSource; + emptyDir: EmptyDirSource; + fc: FiberChannelSource; + flocker: FlokerSource; + flexVolume: FlexVolumeSource; + gcePersistentDisk: GcePersistentDiskSource; + gitRepo: GitRepoSource; + glusterfs: GlusterFsSource; + hostPath: HostPathSource; + iscsi: IscsiSource; + local: LocalSource; + nfs: NetworkFsSource; + persistentVolumeClaim: PVCSource; + photonPersistentDisk: PhotonPersistentDiskSource; + portworxVolume: PortworxVolumeSource; + projected: ProjectedSource; + quobyte: QuobyteSource; + rbd: RbdSource; + scaleIO: ScaleIoSource; + secret: SecretSource; + storageos: StorageOsSource; + vsphereVolume: VsphereVolumeSource; + csi: ContainerStorageInterfaceSource; +} + +/** + * The valid kinds of volume + */ +export type PodVolumeKind = keyof PodVolumeVariants; + +export type PodVolume = RequireExactlyOne & { + name: string; +}; + export class Pod extends WorkloadKubeObject { static kind = "Pod"; static namespaced = true; @@ -206,23 +644,7 @@ export class Pod extends WorkloadKubeObject { } declare spec?: { - volumes?: { - name: string; - persistentVolumeClaim: { - claimName: string; - }; - emptyDir: { - medium?: string; - sizeLimit?: string; - }; - configMap: { - name: string; - }; - secret: { - secretName: string; - defaultMode: number; - }; - }[]; + volumes?: PodVolume[]; initContainers: IPodContainer[]; containers: IPodContainer[]; restartPolicy?: string; @@ -239,9 +661,7 @@ export class Pod extends WorkloadKubeObject { [selector: string]: string; }; securityContext?: {}; - imagePullSecrets?: { - name: string; - }[]; + imagePullSecrets?: LocalObjectReference[]; hostNetwork?: boolean; hostPID?: boolean; hostIPC?: boolean; diff --git a/src/common/k8s-api/endpoints/secret.api.ts b/src/common/k8s-api/endpoints/secret.api.ts index a3b821fcf8..93953ebf48 100644 --- a/src/common/k8s-api/endpoints/secret.api.ts +++ b/src/common/k8s-api/endpoints/secret.api.ts @@ -25,6 +25,11 @@ export interface ISecretRef { name: string; } +export interface SecretReference { + name: string; + namespace?: string; +} + export interface SecretData extends KubeJsonApiData { type: SecretType; data?: Record; diff --git a/src/common/k8s-api/kube-object.ts b/src/common/k8s-api/kube-object.ts index afabeaf638..8e1b6daee2 100644 --- a/src/common/k8s-api/kube-object.ts +++ b/src/common/k8s-api/kube-object.ts @@ -21,6 +21,13 @@ export type KubeObjectConstructor = (new (data: KubeJsonAp apiBase?: string; }; +/** + * A reference to an object in the same namespace + */ +export interface LocalObjectReference { + name: string; +} + export interface KubeObjectMetadata { uid: string; name: string; diff --git a/src/renderer/components/+workloads-pods/pod-details-secrets.tsx b/src/renderer/components/+workloads-pods/pod-details-secrets.tsx index d7affcef66..c7985bb9aa 100644 --- a/src/renderer/components/+workloads-pods/pod-details-secrets.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-secrets.tsx @@ -5,10 +5,10 @@ import "./pod-details-secrets.scss"; -import React, { Component } from "react"; +import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; -import { autorun, observable, makeObservable } from "mobx"; -import { disposeOnUnmount, observer } from "mobx-react"; +import { autorun, observable } from "mobx"; +import { observer } from "mobx-react"; import { Pod, Secret, secretsApi } from "../../../common/k8s-api/endpoints"; import { getDetailsUrl } from "../kube-detail-params"; @@ -16,59 +16,38 @@ export interface PodDetailsSecretsProps { pod: Pod; } -@observer -export class PodDetailsSecrets extends Component { - @observable secrets: Map = observable.map(); +export const PodDetailsSecrets = observer(({ pod }: PodDetailsSecretsProps) => { + const [secrets] = useState(observable.map()); - componentDidMount(): void { - disposeOnUnmount(this, [ - autorun(async () => { - const { pod } = this.props; - - const secrets = await Promise.all( - pod.getSecrets().map(secretName => secretsApi.get({ - name: secretName, - namespace: pod.getNs(), - })), - ); - - secrets.forEach(secret => secret && this.secrets.set(secret.getName(), secret)); - }), - ]); - } - - constructor(props: PodDetailsSecretsProps) { - super(props); - makeObservable(this); - } - - render() { - const { pod } = this.props; - - return ( -
- { - pod.getSecrets().map(secretName => { - const secret = this.secrets.get(secretName); - - if (secret) { - return this.renderSecretLink(secret); - } else { - return ( - {secretName} - ); - } - }) - } -
+ useEffect(() => autorun(async () => { + const podSecrets = await Promise.all( + pod.getSecrets().map(secretName => secretsApi.get({ + name: secretName, + namespace: pod.getNs(), + })), ); - } - protected renderSecretLink(secret: Secret) { + secrets.replace(podSecrets.filter(Boolean).map(secret => [secret.getName(), secret])); + }), []); + + const renderSecret = (name: string) => { + const secret = secrets.get(name); + + if (!secret) { + return {name}; + } + return ( {secret.getName()} ); - } -} + }; + + return ( +
+ {pod.getSecrets().map(renderSecret)} +
+ ); +}); + diff --git a/src/renderer/components/+workloads-pods/pod-details-volumes.tsx b/src/renderer/components/+workloads-pods/pod-details-volumes.tsx new file mode 100644 index 0000000000..add43fce69 --- /dev/null +++ b/src/renderer/components/+workloads-pods/pod-details-volumes.tsx @@ -0,0 +1,647 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import jsyaml from "js-yaml"; +import { observer } from "mobx-react"; +import React from "react"; +import { Link } from "react-router-dom"; +import { configMapApi, Pod, PodVolume, PodVolumeKind, PodVolumeVariants, pvcApi, SecretReference, secretsApi } from "../../../common/k8s-api/endpoints"; +import type { KubeApi } from "../../../common/k8s-api/kube-api"; +import type { KubeObject, LocalObjectReference } from "../../../common/k8s-api/kube-object"; +import { DrawerItem, DrawerItemLabels, DrawerSection, DrawerSubSection } from "../drawer"; +import { Icon } from "../icon"; +import { getDetailsUrl } from "../kube-detail-params"; + +/** + * Additional parameters for each volume variant + */ +export interface VariantOptions { + pod: Pod; + volumeName: string; +} + +function renderLocalRef(pod: Pod, title: string, ref: LocalObjectReference | SecretReference, typeApi: KubeApi) { + return ( + + + {ref.name} + + + ); +} + +/** + * + */ +type PodVolumeVariantRenderers = { + [Key in keyof PodVolumeVariants]: (variant: PodVolumeVariants[Key], opts: VariantOptions) => React.ReactNode; +}; + +const volumeRenderers: PodVolumeVariantRenderers = { + awsElasticBlockStore: ({ volumeID, fsType = "ext4" }) => ( + <> + + {volumeID} + + + {fsType} + + + ), + azureDisk: ({ diskName, diskURI, kind = "Shared", cachingMode = "None", fsType = "ext4", readonly = false }) => ( + <> + + {diskName} + + + {diskURI} + + + {kind} + + + {cachingMode} + + + {fsType} + + + {readonly.toString()} + + + ), + azureFile: ({ readOnly = false, secretName, shareName, secretNamespace = "default" }) => ( + <> + + {secretName} + + + {shareName} + + + {secretNamespace} + + + {readOnly.toString()} + + + ), + ephemeral: ({ volumeClaimTemplate: { metadata, spec }}, { pod, volumeName }) => ( + <> + + {pod.getName()}-{volumeName} + + + + + {jsyaml.dump(spec)} + + + ), + emptyDir: ({ medium, sizeLimit }) => ( + <> + + + + ), + cephfs: ({ monitors, path = "/", user = "admin", secretFile = "/etc/ceph/user.secret", secretRef, readOnly }, { pod }) => ( + <> + +
    + {monitors.map(monitor =>
  • {monitor}
  • )} +
+
+ + {path} + + + {user} + + { + secretRef + ? renderLocalRef(pod, "Secret", secretRef, secretsApi) + : ( + + {secretFile} + + ) + } + + {readOnly.toString()} + + + ), + cinder: ({ volumeID, fsType = "ext4" }) => ( + <> + + {volumeID} + + + {fsType} + + + ), + configMap: ({ name }, { pod }) => renderLocalRef(pod, "Name", { name }, configMapApi), + downwardAPI: ({ items }) => ( + <> + +
    + {items.map(item =>
  • {item.path}
  • )} +
+
+ + ), + fc: ({ targetWWNs, lun, fsType = "ext4", readOnly = false }) => ( + <> + +
    + {targetWWNs.map(targetWWN =>
  • {targetWWN}
  • )} +
+
+ + {lun.toString()} + + + {fsType} + + + {readOnly.toString()} + + + ), + flexVolume: ({ driver, fsType, secretRef, readOnly = false, options }, { pod }) => ( + <> + + {driver} + + + {fsType || "-- system default --"} + + {secretRef && renderLocalRef(pod, "Secret", secretRef, secretsApi)} + + {readOnly.toString()} + + { + ...Object.entries(options) + .map(([key, value]) => ( + + {value} + + )) + } + + ), + flocker: ({ datasetName }) => ( + <> + + {datasetName} + + + ), + gcePersistentDisk: ({ pdName, fsType = "ext4" }) => ( + <> + + {pdName} + + + {fsType} + + + ), + gitRepo: ({ repository, revision }) => ( + <> + + {repository} + + + {revision} + + + ), + glusterfs: ({ endpoints, path, readOnly = false }) => ( + <> + + {endpoints} + + + {path} + + + {readOnly.toString()} + + + ), + hostPath: ({ path, type }) => ( + <> + + {path} + + + {type || "-- none --"} + + + ), + iscsi: ({ targetPortal, iqn, lun, fsType = "ext4", readOnly = false, chapAuthDiscovery, chapAuthSession, secretRef }) => ( + <> + + {targetPortal} + + + {iqn} + + + {lun.toString()} + + + {fsType} + + + {readOnly.toString()} + + {chapAuthDiscovery && ( + + {chapAuthDiscovery.toString()} + + )} + {chapAuthSession && ( + + {chapAuthSession.toString()} + + )} + { secretRef && ( + + {secretRef.name} + + )} + + ), + local: ({ path }) => ( + <> + + {path} + + + ), + nfs: ({ server, path, readOnly = false }) => ( + <> + + {server} + + + {path} + + + {readOnly.toString()} + + + ), + persistentVolumeClaim: ({ claimName }, { pod }) => renderLocalRef(pod, "Name", { name: claimName }, pvcApi), + photonPersistentDisk: ({ pdID, fsType = "ext4" }) => ( + <> + + {pdID} + + + {fsType} + + + ), + portworxVolume: ({ volumeID, fsType = "ext4", readOnly = false }) => ( + <> + + {volumeID} + + + {fsType} + + + {readOnly.toString()} + + + ), + projected: ({ sources, defaultMode }) => ( + <> + + 0o{defaultMode.toString(8)} + + + { + sources.map(({ secret, downwardAPI, configMap, serviceAccountToken }, index) => ( + + {secret && ( + + + {secret.name} + + {secret.items && ( + +
    + {secret.items.map(({ key }) =>
  • {key}
  • )} +
+
+ )} +
+ )} + {downwardAPI && ( + + {downwardAPI.items && ( + +
    + {downwardAPI.items.map(({ path }) =>
  • {path}
  • )} +
+
+ )} +
+ )} + {configMap && ( + + + {configMap.name} + + {configMap.items && ( + +
    + {configMap.items.map(({ path }) =>
  • {path}
  • )} +
+
+ )} +
+ )} + {serviceAccountToken && ( + + + + + {serviceAccountToken.path} + + + )} +
+ )) + } +
+ + ), + quobyte: ({ registry, volume, readOnly = false, user = "serviceaccount", group, tenant }) => ( + <> + + {registry} + + + {volume} + + + {readOnly.toString()} + + + {user} + + + {group ?? "-- no group --"} + + + + ), + rbd: ({ monitors, image, fsType = "ext4", pool = "rbd", user = "admin", keyring = "/etc/ceph/keyright", secretRef, readOnly = false }, { pod }) => ( + <> + +
    + {monitors.map(monitor =>
  • {monitor}
  • )} +
+
+ + {image} + + + {fsType} + + + {pool} + + + {user} + + { + secretRef + ? renderLocalRef(pod, "Authentication Secret", secretRef, secretsApi) + : ( + + {keyring} + + ) + } + + {readOnly.toString()} + + + ), + scaleIO: ({ gateway, system, secretRef, sslEnabled = false, protectionDomain, storagePool, storageMode = "ThinProvisioned", volumeName, fsType = "xfs", readOnly = false }, { pod }) => ( + <> + + {gateway} + + + {system} + + {renderLocalRef(pod, "Name", secretRef, secretsApi)} + + {sslEnabled.toString()} + + + {sslEnabled.toString()} + + + + + + {volumeName} + + + {fsType} + + + {readOnly.toString()} + + + ), + secret: ({ secretName, items = [], defaultMode = 0o644, optional = false }, { pod }) => ( + <> + {renderLocalRef(pod, "Name", { name: secretName }, secretsApi)} + + + 0o{defaultMode.toString(8)} + + + {optional.toString()} + + + ), + storageos: ({ volumeName, volumeNamespace, fsType = "ext4", readOnly = false, secretRef }, { pod }) => ( + <> + + {volumeName} + + + + {fsType} + + + {readOnly.toString()} + + {secretRef && renderLocalRef(pod, "Secret", secretRef, secretsApi)} + + ), + vsphereVolume: ({ volumePath, fsType = "ext4", storagePolicyName, storagePolicyID }) => ( + <> + + {volumePath} + + + {fsType} + + + + + ), + csi: ({ driver, readOnly = false, fsType = "ext4", volumeAttributes = {}, nodePublishSecretRef, controllerPublishSecretRef, nodeStageSecretRef, controllerExpandSecretRef }, { pod }) => ( + <> + + {driver} + + + {readOnly.toString()} + + + {fsType} + + {controllerPublishSecretRef && renderLocalRef(pod, "Controller Publish Secret", controllerPublishSecretRef, secretsApi)} + {controllerExpandSecretRef && renderLocalRef(pod, "Controller Expand Secret", controllerExpandSecretRef, secretsApi)} + {nodePublishSecretRef && renderLocalRef(pod, "Node Publish Secret", nodePublishSecretRef, secretsApi)} + {nodeStageSecretRef && renderLocalRef(pod, "Node Stage Secret", nodeStageSecretRef, secretsApi)} + { + ...Object.entries(volumeAttributes) + .map(([key, value]) => ( + + {value} + + )) + } + + ), +}; + +export interface PodVolumesProps { + pod: Pod; +} + +const deprecatedVolumeTypes = new Set([ + "flocker", + "gitRepo", + "quobyte", + "storageos", +]); + +function getVolumeType(volume: PodVolume): PodVolumeKind | undefined { + const keys = new Set(Object.keys(volume)); + + keys.delete("name"); // This key is not a kind field + + for (const key of keys) { + // skip other random keys + if (key in volumeRenderers) { + const kind = key as PodVolumeKind; + + if (volume[kind] && typeof volume[kind] === "object") { + return kind; + } + } + } + + return undefined; +} + +export const PodVolumes = observer(({ pod }: PodVolumesProps) => { + if (!pod) { + return null; + } + + const renderVolume = (volume: PodVolume) => { + const type = getVolumeType(volume); + const isDeprecated = deprecatedVolumeTypes.has(type); + const renderVolume = volumeRenderers[type]; + + return ( +
+
+ + {volume.name} +
+ { + renderVolume + ? ( + <> + + {type} + {isDeprecated && } + + {renderVolume(volume[type] as any, { pod, volumeName: volume.name })} + + ) + : type + ? ( + + {type} + + ) + :

Error! Unknown pod volume kind

+ } +
+ ); + }; + + const volumes = pod.getVolumes(); + + return ( + + ); +}); diff --git a/src/renderer/components/+workloads-pods/pod-details.tsx b/src/renderer/components/+workloads-pods/pod-details.tsx index 2b592ebe04..7ff23d83da 100644 --- a/src/renderer/components/+workloads-pods/pod-details.tsx +++ b/src/renderer/components/+workloads-pods/pod-details.tsx @@ -10,14 +10,13 @@ import kebabCase from "lodash/kebabCase"; import { disposeOnUnmount, observer } from "mobx-react"; import { Link } from "react-router-dom"; import { observable, reaction, makeObservable } from "mobx"; -import { type IPodMetrics, nodesApi, Pod, pvcApi, configMapApi, getMetricsForPods } from "../../../common/k8s-api/endpoints"; -import { DrawerItem, DrawerTitle } from "../drawer"; +import { type IPodMetrics, nodesApi, Pod, getMetricsForPods } from "../../../common/k8s-api/endpoints"; +import { DrawerItem, DrawerSection } from "../drawer"; import { Badge } from "../badge"; import { boundMethod, cssNames, toJS } from "../../utils"; import { PodDetailsContainer } from "./pod-details-container"; import { PodDetailsAffinities } from "./pod-details-affinities"; import { PodDetailsTolerations } from "./pod-details-tolerations"; -import { Icon } from "../icon"; import { PodDetailsSecrets } from "./pod-details-secrets"; import { ResourceMetrics } from "../resource-metrics"; import type { KubeObjectDetailsProps } from "../kube-object-details"; @@ -28,6 +27,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { getDetailsUrl } from "../kube-detail-params"; import logger from "../../../common/logger"; +import { PodVolumes } from "./pod-details-volumes"; export interface PodDetailsProps extends KubeObjectDetailsProps { } @@ -73,12 +73,12 @@ export class PodDetails extends React.Component { } const { status, spec } = pod; - const { conditions, podIP } = status; + const { conditions = [], podIP } = status ?? {}; const podIPs = pod.getIPs(); - const { nodeName } = spec; + const { nodeName } = spec ?? {}; const nodeSelector = pod.getNodeSelectors(); - const volumes = pod.getVolumes(); const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Pod); + const initContainers = pod.getInitContainers(); return (
@@ -90,26 +90,22 @@ export class PodDetails extends React.Component { )} + + {pod.getStatusMessage()} - - {nodeName && ( - - {nodeName} - - )} + {podIP} -
); } diff --git a/src/renderer/components/drawer/drawer-item-labels.tsx b/src/renderer/components/drawer/drawer-item-labels.tsx index c3dfb41bfc..e25e615910 100644 --- a/src/renderer/components/drawer/drawer-item-labels.tsx +++ b/src/renderer/components/drawer/drawer-item-labels.tsx @@ -6,21 +6,30 @@ import React from "react"; import { DrawerItem, DrawerItemProps } from "./drawer-item"; import { Badge } from "../badge"; +import { KubeObject } from "../../../common/k8s-api/kube-object"; export interface DrawerItemLabelsProps extends DrawerItemProps { - labels: string[]; + labels: string[] | Record; } export function DrawerItemLabels(props: DrawerItemLabelsProps) { const { labels, ...itemProps } = props; - if (!labels || !labels.length) { + if (!labels || typeof labels !== "object") { + return null; + } + + const labelStrings = Array.isArray(labels) + ? labels + : KubeObject.stringifyLabels(labels); + + if (labelStrings.length === 0) { return null; } return ( - {labels.map(label => )} + {labelStrings.map(label => )} ); } diff --git a/src/renderer/components/drawer/drawer-section.tsx b/src/renderer/components/drawer/drawer-section.tsx new file mode 100644 index 0000000000..05bc77e753 --- /dev/null +++ b/src/renderer/components/drawer/drawer-section.tsx @@ -0,0 +1,47 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import React from "react"; +import { DrawerTitle, DrawerSubTitle } from "./drawer-title"; + +export interface DrawerSectionProps { + className?: string; + title?: React.ReactNode; + hidden?: boolean; +} + +export class DrawerSection extends React.Component { + render() { + const { title, children, className, hidden } = this.props; + + if (hidden) { + return null; + } + + return ( + <> + + {children} + + ); + } +} + +export class DrawerSubSection extends React.Component { + render() { + const { title, children, className, hidden } = this.props; + + if (hidden) { + return null; + } + + return ( + <> + + {children} + + ); + } +} diff --git a/src/renderer/components/drawer/drawer-title.scss b/src/renderer/components/drawer/drawer-title.scss index 74e525f1d2..8a2f77caba 100644 --- a/src/renderer/components/drawer/drawer-title.scss +++ b/src/renderer/components/drawer/drawer-title.scss @@ -8,3 +8,8 @@ margin: $margin * 3 (-$margin * 3); background: var(--drawerSubtitleBackground); } + +.DrawerSubTitle { + padding: $padding $padding * 2; + background: var(--drawerSubtitleBackground); +} diff --git a/src/renderer/components/drawer/drawer-title.tsx b/src/renderer/components/drawer/drawer-title.tsx index 3ef2858c9b..6a7efeca11 100644 --- a/src/renderer/components/drawer/drawer-title.tsx +++ b/src/renderer/components/drawer/drawer-title.tsx @@ -23,3 +23,14 @@ export class DrawerTitle extends React.Component { ); } } +export class DrawerSubTitle extends React.Component { + render() { + const { title, children, className } = this.props; + + return ( +
+ {title || children} +
+ ); + } +} diff --git a/src/renderer/components/drawer/index.ts b/src/renderer/components/drawer/index.ts index 67fb08b65b..e7a4bbffbd 100644 --- a/src/renderer/components/drawer/index.ts +++ b/src/renderer/components/drawer/index.ts @@ -5,6 +5,7 @@ export * from "./drawer"; export * from "./drawer-title"; +export * from "./drawer-section"; export * from "./drawer-item"; export * from "./drawer-item-labels"; export * from "./drawer-param-toggler"; diff --git a/src/renderer/components/tabs/tabs.tsx b/src/renderer/components/tabs/tabs.tsx index 7c198ca08f..d640ce66f0 100644 --- a/src/renderer/components/tabs/tabs.tsx +++ b/src/renderer/components/tabs/tabs.tsx @@ -17,8 +17,6 @@ interface TabsContextValue { onChange?(value: D): void; } -type Omit = Pick>; - export interface TabsProps extends TabsContextValue, Omit, "onChange"> { className?: string; center?: boolean;