From db2664a82ee0995f350f68d6a72b6ba74e390e5b Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Thu, 16 Jun 2022 09:52:57 -0400 Subject: [PATCH] Readd space between key and value within ContainerEnv - Add some snapshot tests Signed-off-by: Sebastian Malton --- .../+cluster/cluster-no-metrics.tsx | 2 +- .../components/+config-secrets/secret-key.tsx | 39 +++ src/renderer/components/+namespaces/route.tsx | 2 +- .../pod-container-env.test.tsx.snap | 174 +++++++++++ .../__tests__/pod-container-env.test.tsx | 269 ++++++++++++++++++ .../+workloads-pods/pod-container-env.tsx | 137 ++++----- src/renderer/components/animate/animate.tsx | 2 +- src/renderer/components/layout/sidebar.tsx | 2 +- .../notifications/notifications.store.tsx | 13 +- .../show-error-notification.injectable.ts | 13 +- .../frames/cluster-frame/cluster-frame.tsx | 2 +- 11 files changed, 553 insertions(+), 102 deletions(-) create mode 100644 src/renderer/components/+config-secrets/secret-key.tsx create mode 100644 src/renderer/components/+workloads-pods/__tests__/__snapshots__/pod-container-env.test.tsx.snap create mode 100644 src/renderer/components/+workloads-pods/__tests__/pod-container-env.test.tsx diff --git a/src/renderer/components/+cluster/cluster-no-metrics.tsx b/src/renderer/components/+cluster/cluster-no-metrics.tsx index ec039835fc..da5a63cb48 100644 --- a/src/renderer/components/+cluster/cluster-no-metrics.tsx +++ b/src/renderer/components/+cluster/cluster-no-metrics.tsx @@ -22,7 +22,7 @@ interface Dependencies { clusterId: string | undefined; } -export function NonInjectedClusterNoMetrics({ className, navigateToEntitySettings, clusterId }: Dependencies & ClusterNoMetricsProps) { +function NonInjectedClusterNoMetrics({ className, navigateToEntitySettings, clusterId }: Dependencies & ClusterNoMetricsProps) { function openMetricSettingsPage() { if (clusterId) { navigateToEntitySettings(clusterId, "metrics"); diff --git a/src/renderer/components/+config-secrets/secret-key.tsx b/src/renderer/components/+config-secrets/secret-key.tsx new file mode 100644 index 0000000000..d91a5dadc7 --- /dev/null +++ b/src/renderer/components/+config-secrets/secret-key.tsx @@ -0,0 +1,39 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import React, { useState } from "react"; +import type { Secret } from "../../../common/k8s-api/endpoints"; +import { base64, prevDefault } from "../../utils"; +import { Icon } from "../icon"; + +export interface SecretKeyProps { + secret: Secret; + key: string; +} + +export const SecretKey = ({ secret, key }: SecretKeyProps) => { + const [showValue, setShowValue] = useState(false); + + const showKey = () => setShowValue(true); + + const value = secret?.data?.[key]; + + if (showValue && value) { + return <>{base64.decode(value)}; + } + + return ( + <> + {`secretKeyRef(${name}.${key})`} +   + + + ); +}; diff --git a/src/renderer/components/+namespaces/route.tsx b/src/renderer/components/+namespaces/route.tsx index 4bcc59041b..0f8acdbe74 100644 --- a/src/renderer/components/+namespaces/route.tsx +++ b/src/renderer/components/+namespaces/route.tsx @@ -30,7 +30,7 @@ interface Dependencies { openAddNamespaceDialog: () => void; } -export const NonInjectedNamespacesRoute = ({ namespaceStore, openAddNamespaceDialog }: Dependencies) => ( +const NonInjectedNamespacesRoute = ({ namespaceStore, openAddNamespaceDialog }: Dependencies) => ( renders 1`] = ` + +
+
+ + Environment + + +
+
+ +`; + +exports[` renders both env and configMapRef envFrom 1`] = ` + +
+
+ + Environment + + +
+ + foobar + + = + https://localhost:12345 +
+
+ + configFoo + + = + configBar +
+
+
+
+ +`; + +exports[` renders env 1`] = ` + +
+
+ + Environment + + +
+ + foobar + + = + https://localhost:12345 +
+
+
+
+ +`; + +exports[` renders env 2`] = ` + +
+
+ + Environment + + +
+ + foobar + + = + https://localhost:12345 +
+
+
+
+ +`; + +exports[` renders envFrom when given a configMapRef 1`] = ` + +
+
+ + Environment + + +
+ + configFoo + + = + configBar +
+
+
+
+ +`; + +exports[` renders envFrom when given a secretRef 1`] = ` + +
+
+ + Environment + + +
+
+ +`; diff --git a/src/renderer/components/+workloads-pods/__tests__/pod-container-env.test.tsx b/src/renderer/components/+workloads-pods/__tests__/pod-container-env.test.tsx new file mode 100644 index 0000000000..471d61313c --- /dev/null +++ b/src/renderer/components/+workloads-pods/__tests__/pod-container-env.test.tsx @@ -0,0 +1,269 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import React from "react"; +import type { ConfigMapStore } from "../../+config-maps/store"; +import configMapStoreInjectable from "../../+config-maps/store.injectable"; +import type { SecretStore } from "../../+config-secrets/store"; +import secretStoreInjectable from "../../+config-secrets/store.injectable"; +import type { PodContainer } from "../../../../common/k8s-api/endpoints"; +import { Secret, ConfigMap, Pod, SecretType } from "../../../../common/k8s-api/endpoints"; +import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; +import type { DiRender } from "../../test-utils/renderFor"; +import { renderFor } from "../../test-utils/renderFor"; +import { ContainerEnvironment } from "../pod-container-env"; + +describe("", () => { + let render: DiRender; + let secretStore: jest.Mocked>; + let configMapStore: jest.Mocked>; + + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + secretStore = ({ + load: jest.fn(), + getByName: jest.fn(), + }); + configMapStore = ({ + load: jest.fn(), + getByName: jest.fn(), + }); + + di.override(secretStoreInjectable, () => secretStore as jest.Mocked); + di.override(configMapStoreInjectable, () => configMapStore as jest.Mocked); + + render = renderFor(di); + }); + + it("renders", () => { + const container: PodContainer = { + image: "my-image", + name: "my-first-container", + }; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + selfLink: "/api/v1/pods/default/my-pod", + uid: "1234", + }, + spec: { + containers: [container], + }, + }); + const result = render(); + + expect(result.baseElement).toMatchSnapshot(); + }); + + it("renders env", () => { + const container: PodContainer = { + image: "my-image", + name: "my-first-container", + env: [{ + name: "foobar", + value: "https://localhost:12345", + }], + }; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + selfLink: "/api/v1/pods/default/my-pod", + uid: "1234", + }, + spec: { + containers: [container], + }, + }); + const result = render(); + + expect(result.baseElement).toMatchSnapshot(); + }); + + it("renders envFrom when given a configMapRef", () => { + configMapStore.getByName.mockImplementation((name, namespace) => { + expect(name).toBe("my-config-map"); + expect(namespace).toBe("default"); + + return new ConfigMap({ + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + name: "my-config-map", + namespace: "default", + resourceVersion: "2", + selfLink: "/api/v1/configmaps/default/my-config-map", + uid: "456", + }, + data: { + configFoo: "configBar", + }, + }); + }); + + const container: PodContainer = { + image: "my-image", + name: "my-first-container", + envFrom: [{ + configMapRef: { + name: "my-config-map", + }, + }], + }; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + selfLink: "/api/v1/pods/default/my-pod", + uid: "1234", + }, + spec: { + containers: [container], + }, + }); + const result = render(); + + expect(result.baseElement).toMatchSnapshot(); + }); + + it("renders envFrom when given a secretRef", () => { + secretStore.getByName.mockImplementation((name, namespace) => { + expect(name).toBe("my-secret"); + expect(namespace).toBe("default"); + + return new Secret({ + apiVersion: "v1", + kind: "Secret", + metadata: { + name: "my-secret", + namespace: "default", + resourceVersion: "3", + selfLink: "/api/v1/secrets/default/my-secret", + uid: "237", + }, + type: SecretType.BasicAuth, + }); + }); + + const container: PodContainer = { + image: "my-image", + name: "my-first-container", + envFrom: [{ + secretRef: { + name: "my-secret", + }, + }], + }; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + selfLink: "/api/v1/pods/default/my-pod", + uid: "1234", + }, + spec: { + containers: [container], + }, + }); + const result = render(); + + expect(result.baseElement).toMatchSnapshot(); + }); + + it("renders env", () => { + const container: PodContainer = { + image: "my-image", + name: "my-first-container", + env: [{ + name: "foobar", + value: "https://localhost:12345", + }], + }; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + selfLink: "/api/v1/pods/default/my-pod", + uid: "1234", + }, + spec: { + containers: [container], + }, + }); + const result = render(); + + expect(result.baseElement).toMatchSnapshot(); + }); + + it("renders both env and configMapRef envFrom", () => { + configMapStore.getByName.mockImplementation((name, namespace) => { + expect(name).toBe("my-config-map"); + expect(namespace).toBe("default"); + + return new ConfigMap({ + apiVersion: "v1", + kind: "ConfigMap", + metadata: { + name: "my-config-map", + namespace: "default", + resourceVersion: "2", + selfLink: "/api/v1/configmaps/default/my-config-map", + uid: "456", + }, + data: { + configFoo: "configBar", + }, + }); + }); + + const container: PodContainer = { + image: "my-image", + name: "my-first-container", + envFrom: [{ + configMapRef: { + name: "my-config-map", + }, + }], + env: [{ + name: "foobar", + value: "https://localhost:12345", + }], + }; + const pod = new Pod({ + apiVersion: "v1", + kind: "Pod", + metadata: { + name: "my-pod", + namespace: "default", + resourceVersion: "1", + selfLink: "/api/v1/pods/default/my-pod", + uid: "1234", + }, + spec: { + containers: [container], + }, + }); + const result = render(); + + expect(result.baseElement).toMatchSnapshot(); + }); +}); diff --git a/src/renderer/components/+workloads-pods/pod-container-env.tsx b/src/renderer/components/+workloads-pods/pod-container-env.tsx index 384ea3dd7e..f32b562d81 100644 --- a/src/renderer/components/+workloads-pods/pod-container-env.tsx +++ b/src/renderer/components/+workloads-pods/pod-container-env.tsx @@ -5,26 +5,39 @@ import "./pod-container-env.scss"; -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { observer } from "mobx-react"; -import type { PodContainer, Secret } from "../../../common/k8s-api/endpoints"; +import type { PodContainer } from "../../../common/k8s-api/endpoints"; import { DrawerItem } from "../drawer"; import { autorun } from "mobx"; -import { secretStore } from "../+config-secrets/legacy-store"; -import { configMapStore } from "../+config-maps/legacy-store"; -import { Icon } from "../icon"; -import { base64, cssNames, iter } from "../../utils"; +import { iter } from "../../utils"; import _ from "lodash"; +import type { SecretStore } from "../+config-secrets/store"; +import type { ConfigMapStore } from "../+config-maps/store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import configMapStoreInjectable from "../+config-maps/store.injectable"; +import secretStoreInjectable from "../+config-secrets/store.injectable"; +import { SecretKey } from "../+config-secrets/secret-key"; export interface ContainerEnvironmentProps { container: PodContainer; namespace: string; } -export const ContainerEnvironment = observer((props: ContainerEnvironmentProps) => { - const { container: { env, envFrom = [] }, namespace } = props; +interface Dependencies { + secretStore: SecretStore; + configMapStore: ConfigMapStore; +} - useEffect( () => autorun(() => { +const NonInjectedContainerEnvironment = observer((props: ContainerEnvironmentProps & Dependencies) => { + const { + container: { env, envFrom }, + namespace, + configMapStore, + secretStore, + } = props; + + useEffect(() => autorun(() => { for (const { valueFrom } of env ?? []) { if (valueFrom?.configMapKeyRef) { configMapStore.load({ name: valueFrom.configMapKeyRef.name, namespace }); @@ -63,28 +76,32 @@ export const ContainerEnvironment = observer((props: ContainerEnvironmentProps) } if (secretKeyRef) { - secretValue = ( - - ); + const secret = secretStore.getByName(secretKeyRef.name, namespace); + + if (secret) { + secretValue = ( + + ); + } } if (configMapKeyRef) { const { name, key } = configMapKeyRef; const configMap = configMapStore.getByName(name, namespace); - secretValue = configMap ? - configMap.data[key] : - `configMapKeyRef(${name}${key})`; + secretValue = configMap + ? configMap.data[key] + : `configMapKeyRef(${name}${key})`; } } return (
{name} - : + {`= `} {secretValue}
); @@ -92,7 +109,7 @@ export const ContainerEnvironment = observer((props: ContainerEnvironmentProps) }; const renderEnvFrom = () => { - return Array.from(iter.filterFlatMap(envFrom, vars => { + return Array.from(iter.filterFlatMap(envFrom ?? [], vars => { if (vars.configMapRef?.name) { return renderEnvFromConfigMap(vars.configMapRef.name); } @@ -113,7 +130,7 @@ export const ContainerEnvironment = observer((props: ContainerEnvironmentProps) return Object.entries(configMap.data).map(([name, value]) => (
{name} - : + {`= `} {value}
)); @@ -124,75 +141,31 @@ export const ContainerEnvironment = observer((props: ContainerEnvironmentProps) if (!secret) return null; - return Object.keys(secret.data).map(key => { - const secretKeyRef = { - name: secret.getName(), - key, - }; - - const value = ( - - ); - - return ( + return Object.keys(secret.data) + .map(key => (
{key} - : - {value} + {`= `} +
- ); - }); + )); }; return ( - {env && renderEnv()} - {envFrom && renderEnvFrom()} + {renderEnv()} + {renderEnvFrom()} ); }); -export interface SecretKeyProps { - reference: { - name: string; - key: string; - }; - namespace: string; -} - -const SecretKey = (props: SecretKeyProps) => { - const { reference: { name, key }, namespace } = props; - const [loading, setLoading] = useState(false); - const [secret, setSecret] = useState(); - - const showKey = async (evt: React.MouseEvent) => { - evt.preventDefault(); - setLoading(true); - const secret = await secretStore.load({ name, namespace }); - - setLoading(false); - setSecret(secret); - }; - - const value = secret?.data?.[key]; - - if (value) { - return <>{base64.decode(value)}; - } - - return ( - <> - {`secretKeyRef(${name}.${key})`} -   - - - ); -}; +export const ContainerEnvironment = withInjectables(NonInjectedContainerEnvironment, { + getProps: (di, props) => ({ + ...props, + configMapStore: di.inject(configMapStoreInjectable), + secretStore: di.inject(secretStoreInjectable), + }), +}); diff --git a/src/renderer/components/animate/animate.tsx b/src/renderer/components/animate/animate.tsx index d642a41fa8..450d13d9a9 100644 --- a/src/renderer/components/animate/animate.tsx +++ b/src/renderer/components/animate/animate.tsx @@ -122,7 +122,7 @@ class DefaultedAnimate extends React.Component ; +const NonInjectedAnimate = (props: AnimateProps & Dependencies) => ; export const Animate = withInjectables( NonInjectedAnimate, diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index df973e261a..7fcd1b6793 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -22,7 +22,7 @@ interface Dependencies { entityRegistry: CatalogEntityRegistry; } -export const NonInjectedSidebar = observer(({ +const NonInjectedSidebar = observer(({ sidebarItems, entityRegistry, }: Dependencies) => ( diff --git a/src/renderer/components/notifications/notifications.store.tsx b/src/renderer/components/notifications/notifications.store.tsx index 89061ef4ca..6cc19d52d0 100644 --- a/src/renderer/components/notifications/notifications.store.tsx +++ b/src/renderer/components/notifications/notifications.store.tsx @@ -21,18 +21,15 @@ export enum NotificationStatus { export interface CreateNotificationOptions { id?: NotificationId; - timeout?: number; - onClose?(): void; -} - -export interface Notification { - id?: NotificationId; - message: NotificationMessage; - status?: NotificationStatus; timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide onClose?(): void; // additional logic on when the notification times out or is closed by the "x" } +export interface Notification extends CreateNotificationOptions { + message: NotificationMessage; + status: NotificationStatus; +} + export class NotificationsStore { public notifications = observable.array>([], { deep: false }); diff --git a/src/renderer/components/notifications/show-error-notification.injectable.ts b/src/renderer/components/notifications/show-error-notification.injectable.ts index ca5a1a70da..5cbae252b7 100644 --- a/src/renderer/components/notifications/show-error-notification.injectable.ts +++ b/src/renderer/components/notifications/show-error-notification.injectable.ts @@ -13,13 +13,12 @@ const showErrorNotificationInjectable = getInjectable({ instantiate: (di): ShowNotification => { const notificationsStore = di.inject(notificationsStoreInjectable); - return (message, customOpts = {}) => - notificationsStore.add({ - status: NotificationStatus.ERROR, - timeout: 5000, - message, - ...customOpts, - }); + return (message, customOpts = {}) => notificationsStore.add({ + status: NotificationStatus.ERROR, + timeout: 5000, + message, + ...customOpts, + }); }, }); diff --git a/src/renderer/frames/cluster-frame/cluster-frame.tsx b/src/renderer/frames/cluster-frame/cluster-frame.tsx index d1ce64683f..d9e2ca3b9c 100755 --- a/src/renderer/frames/cluster-frame/cluster-frame.tsx +++ b/src/renderer/frames/cluster-frame/cluster-frame.tsx @@ -22,7 +22,7 @@ interface Dependencies { watchHistoryState: () => () => void; } -export const NonInjectedClusterFrame = observer(({ +const NonInjectedClusterFrame = observer(({ namespaceStore, subscribeStores, childComponents,