From 6a93ef035844c370cf7d38eff3b31261444bf1e6 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 19 Apr 2022 09:46:40 -0400 Subject: [PATCH] more crash fixing Signed-off-by: Sebastian Malton --- src/common/k8s-api/endpoints/secret.api.ts | 11 ++ .../selected-filter-namespaces.injectable.ts | 24 ++++ src/common/utils/index.ts | 2 - src/common/utils/string.ts | 21 ---- src/common/utils/types.ts | 6 + .../add-quota-dialog.tsx | 24 ++-- .../+config-secrets/add-secret-dialog.tsx | 13 ++- .../components/+custom-resources/crd-list.tsx | 26 +++-- .../+helm-charts/helm-chart-details.tsx | 30 ++--- .../+helm-charts/helm-chart.store.ts | 8 +- .../+helm-releases/dialog/dialog.tsx | 13 ++- .../namespace-select-filter-model.tsx | 71 ++++++------ .../+namespaces/namespace-select-filter.tsx | 5 +- .../+namespaces/namespace-select.tsx | 14 +-- .../components/+preferences/application.tsx | 73 +++++++------ .../components/+preferences/editor.tsx | 34 +++--- .../components/+preferences/helm-charts.tsx | 53 +++++---- .../+preferences/kubectl-binaries.tsx | 12 +- .../components/+preferences/terminal.tsx | 31 +++--- .../+cluster-role-bindings/dialog.tsx | 59 +++++++--- .../+role-bindings/dialog.tsx | 53 ++++++--- .../+user-management/+roles/add-dialog.tsx | 2 +- .../+service-accounts/create-dialog.tsx | 2 +- .../activate-entity-command.tsx | 47 ++++---- .../components/cluster-metrics-setting.tsx | 19 ++-- .../components/cluster-prometheus-setting.tsx | 43 ++++---- .../command-palette/command-dialog.tsx | 15 +-- .../components/delete-cluster-dialog/view.tsx | 13 ++- .../components/dock/create-resource/view.tsx | 9 +- .../components/dock/install-chart/view.tsx | 15 ++- .../dock/logs/resource-selector.tsx | 39 ++++--- .../components/dock/upgrade-chart/view.tsx | 26 ++--- .../hotbar/hotbar-remove-command.tsx | 17 ++- .../hotbar/hotbar-rename-command.tsx | 23 ++-- .../hotbar/hotbar-switch-command.tsx | 71 +++++------- .../item-object-list/list-layout.tsx | 10 +- .../components/select/select.test.tsx | 15 +-- src/renderer/components/select/select.tsx | 103 +++++++++++++++--- 38 files changed, 618 insertions(+), 434 deletions(-) create mode 100644 src/common/k8s-api/selected-filter-namespaces.injectable.ts delete mode 100644 src/common/utils/string.ts diff --git a/src/common/k8s-api/endpoints/secret.api.ts b/src/common/k8s-api/endpoints/secret.api.ts index 5bbb574537..8a71d914cd 100644 --- a/src/common/k8s-api/endpoints/secret.api.ts +++ b/src/common/k8s-api/endpoints/secret.api.ts @@ -21,6 +21,17 @@ export enum SecretType { BootstrapToken = "bootstrap.kubernetes.io/token", } +export const reverseSecretTypeMap = { + [SecretType.Opaque]: "Opaque", + [SecretType.ServiceAccountToken]: "ServiceAccountToken", + [SecretType.Dockercfg]: "Dockercfg", + [SecretType.DockerConfigJson]: "DockerConfigJson", + [SecretType.BasicAuth]: "BasicAuth", + [SecretType.SSHAuth]: "SSHAuth", + [SecretType.TLS]: "TLS", + [SecretType.BootstrapToken]: "BootstrapToken", +}; + export interface SecretReference { name: string; namespace?: string; diff --git a/src/common/k8s-api/selected-filter-namespaces.injectable.ts b/src/common/k8s-api/selected-filter-namespaces.injectable.ts new file mode 100644 index 0000000000..52c13569f6 --- /dev/null +++ b/src/common/k8s-api/selected-filter-namespaces.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import namespaceStoreInjectable from "../../renderer/components/+namespaces/store.injectable"; +import { createStoresAndApisInjectionToken } from "./create-stores-apis.token"; + +const selectedFilterNamespacesInjectable = getInjectable({ + id: "selected-filter-namespaces", + instantiate: (di) => { + if (!di.inject(createStoresAndApisInjectionToken)) { + // Dummy value so that this works in all environments + return computed(() => []); + } + + const store = di.inject(namespaceStoreInjectable); + + return computed(() => [...store.contextNamespaces]); + }, +}); + +export default selectedFilterNamespacesInjectable; diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 6bdde405c7..3f9d750b4d 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -49,7 +49,6 @@ import * as tuple from "./tuple"; import * as base64 from "./base64"; import * as object from "./objects"; import * as json from "./json"; -import * as string from "./string"; export { iter, @@ -58,5 +57,4 @@ export { base64, object, json, - string, }; diff --git a/src/common/utils/string.ts b/src/common/utils/string.ts deleted file mode 100644 index 5b535aa384..0000000000 --- a/src/common/utils/string.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -/** - * Transforms `val` so that the first character is uppercased - */ -export function uppercaseFirst(val: string): string { - if (val.length === 0) { - return ""; - } - - if (val.length === 1) { - return val.toUpperCase(); - } - - const [first, ...rest] = val; - - return first.toUpperCase() + rest; -} diff --git a/src/common/utils/types.ts b/src/common/utils/types.ts index 1b38ddc849..32ebe14e33 100644 --- a/src/common/utils/types.ts +++ b/src/common/utils/types.ts @@ -3,6 +3,8 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ +import type { SetRequired } from "type-fest"; + export type RemoveUndefinedFromValues = { [P in keyof K]: NonNullable; }; @@ -23,3 +25,7 @@ export type SingleOrMany = T | T[]; export type IfEquals = (() => G extends T ? 1 : 2) extends (() => G extends U ? 1 : 2) ? Y : N; + +export type MaybeSetRequired = Query extends true + ? SetRequired + : BaseType; 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 16545bc3ab..70e0dcba1f 100644 --- a/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx +++ b/src/renderer/components/+config-resource-quotas/add-quota-dialog.tsx @@ -180,7 +180,7 @@ export class AddQuotaDialog extends React.Component { placeholder="Namespace" themeName="light" className="box grow" - onChange={option => this.namespace = option?.namespace ?? null} + onChange={option => this.namespace = option?.value ?? null} /> @@ -190,26 +190,24 @@ export class AddQuotaDialog extends React.Component { className="quota-select" themeName="light" placeholder="Select a quota.." - options={Object.keys(this.quotas).map(quota => ({ quota }))} - isMulti={false} - value={( - this.quotaSelectValue - ? ({ quota: this.quotaSelectValue }) - : null - )} - onChange={option => this.quotaSelectValue = option?.quota ?? null} - formatOptionLabel={({ quota }) => { - const iconMaterial = this.getQuotaOptionLabelIconMaterial(quota); + options={Object.keys(this.quotas).map(quota => ({ + value: quota, + label: quota, + }))} + value={this.quotaSelectValue} + onChange={option => this.quotaSelectValue = option?.value ?? null} + formatOptionLabel={({ value }) => { + const iconMaterial = this.getQuotaOptionLabelIconMaterial(value); return iconMaterial ? ( {" "} - {quota} + {value} ) - : quota; + : value; }} /> { }; private get secretTypeOptions() { - return object.keys(this.secretTemplate).map(type => ({ type })); + return object.keys(this.secretTemplate).map(type => ({ + value: type, + label: reverseSecretTypeMap[type], + })); } @observable secret = this.secretTemplate; @@ -224,7 +227,7 @@ export class AddSecretDialog extends React.Component { id="secret-namespace-input" themeName="light" value={namespace} - onChange={value => this.namespace = value?.namespace ?? "default"} + onChange={option => this.namespace = option?.value ?? "default"} />
@@ -233,8 +236,8 @@ export class AddSecretDialog extends React.Component { id="secret-input" themeName="light" options={this.secretTypeOptions} - value={({ type })} - onChange={value => this.type = value?.type ?? SecretType.Opaque} + value={type} + onChange={option => this.type = option?.value ?? SecretType.Opaque} />
diff --git a/src/renderer/components/+custom-resources/crd-list.tsx b/src/renderer/components/+custom-resources/crd-list.tsx index d14b554077..1a51c1b99d 100644 --- a/src/renderer/components/+custom-resources/crd-list.tsx +++ b/src/renderer/components/+custom-resources/crd-list.tsx @@ -48,8 +48,17 @@ export class CustomResourceDefinitions extends React.Component { return customResourceDefinitionStore.items; // show all by default } - toggleSelection = (options: readonly ({ group: string })[]) => { - const groups = options.map(({ group }) => group); + @computed get groupSelectOptions() { + return Object.keys(customResourceDefinitionStore.groups) + .map(group => ({ + value: group, + label: group, + isSelected: this.selectedGroups.has(group), + })); + } + + toggleSelection = (options: readonly ({ value: string })[]) => { + const groups = options.map(({ value }) => value); this.selectedGroups.replace(groups); crdGroupsUrlParam.set(groups); @@ -68,8 +77,6 @@ export class CustomResourceDefinitions extends React.Component { } render() { - const { items, selectedGroups } = this; - return ( already has and is always mounted subscribeStores={false} - items={items} + items={this.items} sortingCallbacks={{ [columnId.kind]: crd => crd.getResourceKind(), [columnId.group]: crd => crd.getGroup(), @@ -103,17 +110,16 @@ export class CustomResourceDefinitions extends React.Component { themeStore.themes.get(option.theme)?.name ?? "Sync with computer"} - value={({ theme: userStore.colorTheme })} - onChange={value => userStore.colorTheme = value?.theme ?? defaultTheme} + value={userStore.colorTheme} + onChange={value => userStore.colorTheme = value?.value ?? defaultTheme} themeName="lens" /> @@ -68,19 +90,9 @@ const NonInjectedApplication: React.FC = ({ appPreferenceItems, us userStore.updateChannel = value?.channel ?? defaultUpdateChannel} + value={userStore.updateChannel} + onChange={value => userStore.updateChannel = value?.value ?? defaultUpdateChannel} themeName="lens" /> @@ -141,9 +153,8 @@ const NonInjectedApplication: React.FC = ({ appPreferenceItems, us editorConfiguration.minimap.side = value?.side} + value={editorConfiguration.minimap.side} + onChange={option => editorConfiguration.minimap.side = option?.value} /> @@ -63,9 +70,8 @@ const NonInjectedEditor = observer(({ userStore }: Dependencies) => { opt.name === userStore.downloadMirror)} - onChange={option => userStore.downloadMirror = option?.name ?? defaultPackageMirror} - getOptionLabel={option => option.mirror.label} + value={userStore.downloadMirror} + onChange={option => userStore.downloadMirror = option?.value ?? defaultPackageMirror} isDisabled={!userStore.downloadKubectlBinaries} - 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 cfbaeceb02..8ae211a409 100644 --- a/src/renderer/components/+preferences/terminal.tsx +++ b/src/renderer/components/+preferences/terminal.tsx @@ -23,11 +23,21 @@ interface Dependencies { defaultShell: string; } -const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: Dependencies) => { +const NonInjectedTerminal = observer(({ + userStore, + themeStore, + defaultShell, +}: Dependencies) => { const themeOptions = [ - "", - ...themeStore.themes.keys(), - ].map(name => ({ name })); + { + value: "", // TODO: replace with a sentinal value that isn't string (and serialize it differently) + label: "Match Lens Theme", + }, + ...Array.from(themeStore.themes, ([themeId, { name }]) => ({ + value: themeId, + label: name, + })), + ]; return ( @@ -59,17 +69,8 @@ const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: D `${entity.kind}: ${entity.getName()}`} - autoFocus={true} - escapeClearsValue={false} - placeholder="Activate entity ..." - /> - ); -}); +const NonInjectedActivateEntityCommand = observer(({ closeCommandOverlay, entities }: Dependencies) => ( + { - this.selectedOption = provider ?? { provider: autoDetectPrometheus }; + onChange={option => { + this.selectedOption = option?.value ?? autoDetectPrometheus; this.onSaveProvider(); }} options={this.options} diff --git a/src/renderer/components/command-palette/command-dialog.tsx b/src/renderer/components/command-palette/command-dialog.tsx index 153e7377d1..6cfa073791 100644 --- a/src/renderer/components/command-palette/command-dialog.tsx +++ b/src/renderer/components/command-palette/command-dialog.tsx @@ -36,7 +36,7 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman try { closeCommandOverlay(); - option.action({ + option.value.action({ entity: activeEntity, navigate: (url, opts = {}) => { const { forceRootFrame = false } = opts; @@ -49,7 +49,7 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman }, }); } catch (error) { - console.error("[COMMAND-DIALOG] failed to execute command", option.id, error); + console.error("[COMMAND-DIALOG] failed to execute command", option.value.id, error); } }; @@ -65,6 +65,12 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman return void console.error(`[COMMAND-DIALOG]: isActive for ${command.id} threw an error, defaulting to false`, error); } }) + .map(command => ({ + value: command, + label: typeof command.title === "string" + ? command.title + : command.title(context), + })) .collect(items => Array.from(items)); return ( @@ -78,11 +84,6 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman }} menuIsOpen options={activeCommands} - getOptionLabel={({ title }) => ( - typeof title === "string" - ? title - : title(context) - )} autoFocus={true} escapeClearsValue={false} data-test-id="command-palette-search" diff --git a/src/renderer/components/delete-cluster-dialog/view.tsx b/src/renderer/components/delete-cluster-dialog/view.tsx index d0cb7dc6b4..79e0ea88ee 100644 --- a/src/renderer/components/delete-cluster-dialog/view.tsx +++ b/src/renderer/components/delete-cluster-dialog/view.tsx @@ -99,22 +99,23 @@ class NonInjectedDeleteClusterDialog extends React.Component { return null; } - const contextName = this.newCurrentContext.get(); const selectOptions = config .contexts - .filter(context => context.name !== cluster.contextName); - const selectedOption = selectOptions.find(ctx => ctx.name === contextName); + .filter(context => context.name !== cluster.contextName) + .map(context => ({ + value: context.name, + label: context.name, + })); return (
({ + value: pod, + label: pod.getName(), + })); const allContainers = pod.getAllContainers(); const container = allContainers.find(container => container.name === selectedContainer) ?? null; - const onContainerChange = (container: PodContainer | null) => { - if (!container) { + const onContainerChange = (option: SingleValue>) => { + if (!option) { return; } model.updateLogTabData({ - selectedContainer: container.name, + selectedContainer: option.value.name, }); model.reloadLogs(); }; - const onPodChange = (value: Pod | null) => { - if (!value) { + const onPodChange = (option: SingleValue>) => { + if (!option) { return; } model.updateLogTabData({ - selectedPodId: value.getId(), - selectedContainer: value.getAllContainers()[0]?.name, + selectedPodId: option.value.getId(), + selectedContainer: option.value.getAllContainers()[0]?.name, }); - model.renameTab(`Pod ${value.getName()}`); + model.renameTab(`Pod ${option.value.getName()}`); model.reloadLogs(); }; const containerSelectOptions = [ { label: "Containers", - options: pod.getContainers(), + options: pod.getContainers().map(container => ({ + value: container, + label: container.name, + })), }, { label: "Init Containers", - options: pod.getInitContainers(), + options: pod.getInitContainers().map(container => ({ + value: container, + label: container.name, + })), }, ]; @@ -86,21 +97,19 @@ export const LogResourceSelector = observer(({ model }: LogResourceSelectorProps } Pod { - if (!hotbar) { + onChange={option => { + if (!option) { return; } @@ -36,14 +36,14 @@ const NonInjectedHotbarRemoveCommand = observer(({ primary: false, accent: true, }, - ok: () => hotbarStore.remove(hotbar), + ok: () => hotbarStore.remove(option.value), message: (

Are you sure you want remove hotbar {" "} - {hotbar.name} + {option.value.name} ?

@@ -53,8 +53,13 @@ const NonInjectedHotbarRemoveCommand = observer(({ } } components={{ DropdownIndicator: null, IndicatorSeparator: null }} menuIsOpen={true} - options={hotbarStore.hotbars} - getOptionLabel={hotbar => hotbarStore.getDisplayLabel(hotbar)} + options={( + hotbarStore.hotbars + .map(hotbar => ({ + value: hotbar, + label: hotbarStore.getDisplayLabel(hotbar), + })) + )} autoFocus={true} escapeClearsValue={false} placeholder="Remove hotbar" diff --git a/src/renderer/components/hotbar/hotbar-rename-command.tsx b/src/renderer/components/hotbar/hotbar-rename-command.tsx index 8ef812e1a3..022050ecf2 100644 --- a/src/renderer/components/hotbar/hotbar-rename-command.tsx +++ b/src/renderer/components/hotbar/hotbar-rename-command.tsx @@ -9,7 +9,6 @@ import { Select } from "../select"; import hotbarStoreInjectable from "../../../common/hotbars/store.injectable"; import type { InputValidator } from "../input"; import { Input } from "../input"; -import type { Hotbar } from "../../../common/hotbars/types"; import { withInjectables } from "@ogre-tools/injectable-react"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable"; @@ -29,12 +28,6 @@ const NonInjectedHotbarRenameCommand = observer(({ const [hotbarId, setHotbarId] = useState(""); const [hotbarName, setHotbarName] = useState(""); - const onSelect = (hotbar: Hotbar | null) => { - if (hotbar) { - setHotbarId(hotbar.id); - setHotbarName(hotbar.name); - } - }; const onSubmit = (name: string) => { if (!name.trim()) { return; @@ -69,11 +62,21 @@ const NonInjectedHotbarRenameCommand = observer(({ ", () => { const { container } = render(( ", () => { const { container, rerender } = render(( ", () => { rerender(( ", () => { const { container, rerender } = render(( ", () => { expect(selectedValueContainer?.textContent).toBe(options[0].label); rerender(( - ", () => { const { container, rerender } = render(( ", () => { expect(selectedValueContainer?.textContent).toBe(options[0].label); rerender(( -