diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts index e5248611b2..7939dbc922 100644 --- a/src/common/user-store/preferences-helpers.ts +++ b/src/common/user-store/preferences-helpers.ts @@ -88,7 +88,7 @@ const colorTheme: PreferenceDescription = { }, }; -const terminalTheme: PreferenceDescription = { +const terminalTheme: PreferenceDescription = { fromStore(val) { return val || ""; }, diff --git a/src/common/utils/objects.ts b/src/common/utils/objects.ts index 3554e9aab6..8fe95be46d 100644 --- a/src/common/utils/objects.ts +++ b/src/common/utils/objects.ts @@ -11,6 +11,12 @@ export function fromEntries(entries: Iterable; } +export function keys(obj: Partial>): K[]; + +export function keys(obj: Record): K[] { + return Object.keys(obj) as K[]; +} + export function entries(obj: Partial> | null | undefined): [K, V][]; export function entries(obj: Record | null | undefined): [K, V][]; diff --git a/src/main/__test__/context-handler.test.ts b/src/main/__test__/context-handler.test.ts index f48a8583f7..353f1a507c 100644 --- a/src/main/__test__/context-handler.test.ts +++ b/src/main/__test__/context-handler.test.ts @@ -73,7 +73,7 @@ const clusterStub = { } as unknown as Cluster; describe("ContextHandler", () => { - let createContextHandler: (cluster: Cluster) => ClusterContextHandler; + let createContextHandler: (cluster: Cluster) => ClusterContextHandler | undefined; beforeEach(async () => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index c7ad6e328f..deee67f050 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -19,12 +19,13 @@ import type { DiContainer } from "@ogre-tools/injectable"; import { parse } from "url"; import loggerInjectable from "../../common/logger.injectable"; import type { Logger } from "../../common/logger"; +import assert from "assert"; console = new Console(process.stdout, process.stderr); // fix mockFS describe("kubeconfig manager tests", () => { let clusterFake: Cluster; - let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; + let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager | undefined; let di: DiContainer; let logger: jest.Mocked; @@ -92,6 +93,8 @@ describe("kubeconfig manager tests", () => { it("should create 'temp' kube config with proxy", async () => { const kubeConfManager = createKubeconfigManager(clusterFake); + assert(kubeConfManager, "should actually create one"); + expect(logger.error).not.toBeCalled(); expect(await kubeConfManager.getPath()).toBe(`some-directory-for-temp${path.sep}kubeconfig-foo`); // this causes an intermittent "ENXIO: no such device or address, read" error @@ -107,6 +110,8 @@ describe("kubeconfig manager tests", () => { it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => { const kubeConfManager = createKubeconfigManager(clusterFake); + assert(kubeConfManager, "should actually create one"); + const configPath = await kubeConfManager.getPath(); expect(await fse.pathExists(configPath)).toBe(true); diff --git a/src/main/catalog/index.ts b/src/main/catalog/index.ts index 011cf9cdbc..306d26317d 100644 --- a/src/main/catalog/index.ts +++ b/src/main/catalog/index.ts @@ -4,3 +4,4 @@ */ export * from "./catalog-entity-registry"; +export * from "./entity-registry"; diff --git a/src/main/shell-session/node-shell-session/node-shell-session.ts b/src/main/shell-session/node-shell-session/node-shell-session.ts index 55e9479084..b37e8c25b5 100644 --- a/src/main/shell-session/node-shell-session/node-shell-session.ts +++ b/src/main/shell-session/node-shell-session/node-shell-session.ts @@ -51,7 +51,12 @@ export class NodeShellSession extends ShellSession { data: `Error occurred: ${get(error, "response.body.message", error ? String(error) : "unknown error")}`, }); - throw new ShellOpenError("failed to create node pod", error); + throw new ShellOpenError( + "failed to create node pod", + error instanceof Error + ? { cause: error } + : undefined, + ); } const env = await this.getCachedShellEnv(); diff --git a/src/main/shell-session/shell-session.ts b/src/main/shell-session/shell-session.ts index e96f847c8b..93fcb55dd1 100644 --- a/src/main/shell-session/shell-session.ts +++ b/src/main/shell-session/shell-session.ts @@ -23,8 +23,8 @@ import { stat } from "fs/promises"; import { getOrInsertWith } from "../../common/utils"; export class ShellOpenError extends Error { - constructor(message: string, public cause: unknown) { - super(`${message}: ${cause}`); + constructor(message: string, options?: ErrorOptions) { + super(`${message}`, options); this.name = this.constructor.name; Error.captureStackTrace(this); } diff --git a/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx b/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx index 458a36e819..e31a4d9c69 100644 --- a/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx +++ b/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx @@ -94,20 +94,6 @@ export class AddQuotaDialog extends React.Component { return undefined; } - private formatQuotaOptionLabel = (quota: string) => { - const iconMaterial = this.getQuotaOptionLabelIconMaterial(quota); - - return iconMaterial - ? ( - - - {" "} - {quota} - - ) - : quota; - }; - setQuota = () => { if (!this.quotaSelectValue) return; this.quotas[this.quotaSelectValue] = this.quotaInputValue; @@ -194,7 +180,7 @@ export class AddQuotaDialog extends React.Component { placeholder="Namespace" themeName="light" className="box grow" - onChange={value => this.namespace = value} + onChange={option => this.namespace = option?.namespace ?? null} /> @@ -204,11 +190,27 @@ export class AddQuotaDialog extends React.Component { className="quota-select" themeName="light" placeholder="Select a quota.." - options={Object.keys(this.quotas)} + options={Object.keys(this.quotas).map(quota => ({ quota }))} isMulti={false} - value={this.quotaSelectValue} - onChange={value => this.quotaSelectValue = value} - formatOptionLabel={this.formatQuotaOptionLabel} + value={( + this.quotaSelectValue + ? ({ quota: this.quotaSelectValue }) + : null + )} + onChange={option => this.quotaSelectValue = option?.quota ?? null} + formatOptionLabel={({ quota }) => { + const iconMaterial = this.getQuotaOptionLabelIconMaterial(quota); + + return iconMaterial + ? ( + + + {" "} + {quota} + + ) + : quota; + }} /> { } -interface ISecretTemplateField { +interface SecretTemplateField { key: string; value?: string; required?: boolean; } -interface ISecretTemplate { - [field: string]: ISecretTemplateField[] | undefined; - annotations?: ISecretTemplateField[]; - labels?: ISecretTemplateField[]; - data?: ISecretTemplateField[]; +interface SecretTemplate { + [field: string]: SecretTemplateField[] | undefined; + annotations?: SecretTemplateField[]; + labels?: SecretTemplateField[]; + data?: SecretTemplateField[]; } -type ISecretField = keyof ISecretTemplate; +type ISecretField = keyof SecretTemplate; const dialogState = observable.object({ isOpen: false, @@ -61,7 +61,7 @@ export class AddSecretDialog extends React.Component { dialogState.isOpen = false; } - private secretTemplate: Partial> = { + private secretTemplate: Partial> = { [SecretType.Opaque]: {}, [SecretType.ServiceAccountToken]: { annotations: [ @@ -71,8 +71,8 @@ export class AddSecretDialog extends React.Component { }, }; - get types() { - return Object.keys(this.secretTemplate) as SecretType[]; + private get secretTypeOptions() { + return object.keys(this.secretTemplate).map(type => ({ type })); } @observable secret = this.secretTemplate; @@ -89,7 +89,7 @@ export class AddSecretDialog extends React.Component { AddSecretDialog.close(); }; - private getDataFromFields = (fields: ISecretTemplateField[] = [], processValue: (val: string) => string = (val => val)) => { + private getDataFromFields = (fields: SecretTemplateField[] = [], processValue: (val: string) => string = (val => val)) => { return iter.pipeline(fields) .filterMap(({ key, value }) => ( value @@ -160,7 +160,7 @@ export class AddSecretDialog extends React.Component { tabIndex={item.required ? -1 : 0} readOnly={item.required} value={item.key} - onChange={v => item.key = v} + onChange={v => item.key = v} /> { className="value" placeholder="Value" value={item.value} - onChange={v => item.value = v} + onChange={v => item.value = v} /> { tooltip={item.required ? "Required field" : "Remove field"} className="remove-icon" material="remove_circle_outline" - onClick={() => this.removeField(field, index)} + onClick={() => this.removeField(field, index)} /> ))} @@ -224,7 +224,7 @@ export class AddSecretDialog extends React.Component { id="secret-namespace-input" themeName="light" value={namespace} - onChange={value => this.namespace = value ?? "default"} + onChange={value => this.namespace = value?.namespace ?? "default"} />
@@ -232,9 +232,9 @@ export class AddSecretDialog extends React.Component { themeStore.themes.get(option)?.name ?? "Sync with computer"} - value={userStore.colorTheme} - onChange={value => userStore.colorTheme = value ?? defaultTheme} + options={themeOptions} + getOptionLabel={option => themeStore.themes.get(option.theme)?.name ?? "Sync with computer"} + value={({ theme: userStore.colorTheme })} + onChange={value => userStore.colorTheme = value?.theme ?? defaultTheme} themeName="lens" /> @@ -58,14 +67,10 @@ const NonInjectedApplication: React.FC = ({ appPreferenceItems, us
userStore.updateChannel = value ?? defaultUpdateChannel} + options={updateChannelOptions} + value={({ channel: userStore.updateChannel })} + onChange={value => userStore.updateChannel = value?.channel ?? defaultUpdateChannel} themeName="lens" />
@@ -135,10 +140,10 @@ const NonInjectedApplication: React.FC = ({ appPreferenceItems, us
editorConfiguration.minimap.side = value ?? undefined} + options={minimapPositionOptions} + value={editorConfiguration.minimap.side ? ({ side: editorConfiguration.minimap.side }) : null} + onChange={value => editorConfiguration.minimap.side = value?.side} />
@@ -54,15 +62,10 @@ const NonInjectedEditor = observer(({ userStore }: Dependencies) => {
userStore.downloadMirror = value ?? defaultPackageMirror} - getOptionLabel={mirrorKey => packageMirrors.get(mirrorKey)?.label ?? ""} + options={downloadMirrorOptions} + value={downloadMirrorOptions.find(opt => opt.name === userStore.downloadMirror)} + onChange={option => userStore.downloadMirror = option?.name ?? defaultPackageMirror} + getOptionLabel={option => option.mirror.label} isDisabled={!userStore.downloadKubectlBinaries} - isOptionDisabled={mirrorKey => !packageMirrors.get(mirrorKey)?.platforms.has(process.platform)} + isOptionDisabled={option => option.mirror.platforms.has(process.platform)} themeName="lens" />
diff --git a/src/renderer/components/+preferences/terminal.tsx b/src/renderer/components/+preferences/terminal.tsx index 27f4229c9f..cfbaeceb02 100644 --- a/src/renderer/components/+preferences/terminal.tsx +++ b/src/renderer/components/+preferences/terminal.tsx @@ -24,6 +24,11 @@ interface Dependencies { } const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: Dependencies) => { + const themeOptions = [ + "", + ...themeStore.themes.keys(), + ].map(name => ({ name })); + return (
@@ -53,12 +58,9 @@ const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: D { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { title } = activeCommands.get(commandId)!; - - return typeof title === "string" + options={activeCommands} + getOptionLabel={({ title }) => ( + typeof title === "string" ? title - : title(context); - }} + : title(context) + )} autoFocus={true} escapeClearsValue={false} data-test-id="command-palette-search" diff --git a/src/renderer/components/duration/reactive-duration.tsx b/src/renderer/components/duration/reactive-duration.tsx index 9210f48de9..53b1672f17 100644 --- a/src/renderer/components/duration/reactive-duration.tsx +++ b/src/renderer/components/duration/reactive-duration.tsx @@ -9,7 +9,7 @@ import React from "react"; import { formatDuration } from "../../utils"; export interface ReactiveDurationProps { - timestamp: string; + timestamp: string | undefined; /** * Whether the display string should prefer length over precision @@ -34,6 +34,10 @@ function computeUpdateInterval(creationTimestampEpoch: number): number { } export const ReactiveDuration = observer(({ timestamp, compact = true }: ReactiveDurationProps) => { + if (!timestamp) { + return <>{""}; + } + const creationTimestamp = new Date(timestamp).getTime(); return ( diff --git a/src/renderer/components/kube-object/age.tsx b/src/renderer/components/kube-object/age.tsx index ab1491a024..2d7eed38ec 100644 --- a/src/renderer/components/kube-object/age.tsx +++ b/src/renderer/components/kube-object/age.tsx @@ -21,12 +21,8 @@ export interface KubeObjectAgeProps { } export const KubeObjectAge = ({ object, compact = true }: KubeObjectAgeProps) => ( - object.metadata.creationTimestamp - ? ( - - ) - : <>{""} + ); diff --git a/src/renderer/components/layout/sub-header.tsx b/src/renderer/components/layout/sub-header.tsx index cb8893a5f2..0530fef944 100644 --- a/src/renderer/components/layout/sub-header.tsx +++ b/src/renderer/components/layout/sub-header.tsx @@ -5,12 +5,14 @@ import "./sub-header.scss"; import React from "react"; +import type { SingleOrMany } from "../../utils"; import { cssNames } from "../../utils"; export interface SubHeaderProps { className?: string; withLine?: boolean; // add bottom line compact?: boolean; // no extra padding around content + children: SingleOrMany; } export class SubHeader extends React.Component {