1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

fix type errors, most <Select /> errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-04-12 16:55:38 -04:00
parent d55b363df9
commit 595bb715a9
25 changed files with 203 additions and 182 deletions

View File

@ -88,7 +88,7 @@ const colorTheme: PreferenceDescription<string> = {
}, },
}; };
const terminalTheme: PreferenceDescription<string | undefined> = { const terminalTheme: PreferenceDescription<string> = {
fromStore(val) { fromStore(val) {
return val || ""; return val || "";
}, },

View File

@ -11,6 +11,12 @@ export function fromEntries<T, Key extends string>(entries: Iterable<readonly [K
return Object.fromEntries(entries) as Record<Key, T>; return Object.fromEntries(entries) as Record<Key, T>;
} }
export function keys<K extends keyof any>(obj: Partial<Record<K, any>>): K[];
export function keys<K extends keyof any>(obj: Record<K, any>): K[] {
return Object.keys(obj) as K[];
}
export function entries<K extends string | number | symbol, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][]; export function entries<K extends string | number | symbol, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][];
export function entries<K extends string | number | symbol, V>(obj: Record<K, V> | null | undefined): [K, V][]; export function entries<K extends string | number | symbol, V>(obj: Record<K, V> | null | undefined): [K, V][];

View File

@ -73,7 +73,7 @@ const clusterStub = {
} as unknown as Cluster; } as unknown as Cluster;
describe("ContextHandler", () => { describe("ContextHandler", () => {
let createContextHandler: (cluster: Cluster) => ClusterContextHandler; let createContextHandler: (cluster: Cluster) => ClusterContextHandler | undefined;
beforeEach(async () => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = getDiForUnitTesting({ doGeneralOverrides: true });

View File

@ -19,12 +19,13 @@ import type { DiContainer } from "@ogre-tools/injectable";
import { parse } from "url"; import { parse } from "url";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import type { Logger } from "../../common/logger"; import type { Logger } from "../../common/logger";
import assert from "assert";
console = new Console(process.stdout, process.stderr); // fix mockFS console = new Console(process.stdout, process.stderr); // fix mockFS
describe("kubeconfig manager tests", () => { describe("kubeconfig manager tests", () => {
let clusterFake: Cluster; let clusterFake: Cluster;
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager | undefined;
let di: DiContainer; let di: DiContainer;
let logger: jest.Mocked<Logger>; let logger: jest.Mocked<Logger>;
@ -92,6 +93,8 @@ describe("kubeconfig manager tests", () => {
it("should create 'temp' kube config with proxy", async () => { it("should create 'temp' kube config with proxy", async () => {
const kubeConfManager = createKubeconfigManager(clusterFake); const kubeConfManager = createKubeconfigManager(clusterFake);
assert(kubeConfManager, "should actually create one");
expect(logger.error).not.toBeCalled(); expect(logger.error).not.toBeCalled();
expect(await kubeConfManager.getPath()).toBe(`some-directory-for-temp${path.sep}kubeconfig-foo`); 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 // 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 () => { it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => {
const kubeConfManager = createKubeconfigManager(clusterFake); const kubeConfManager = createKubeconfigManager(clusterFake);
assert(kubeConfManager, "should actually create one");
const configPath = await kubeConfManager.getPath(); const configPath = await kubeConfManager.getPath();
expect(await fse.pathExists(configPath)).toBe(true); expect(await fse.pathExists(configPath)).toBe(true);

View File

@ -4,3 +4,4 @@
*/ */
export * from "./catalog-entity-registry"; export * from "./catalog-entity-registry";
export * from "./entity-registry";

View File

@ -51,7 +51,12 @@ export class NodeShellSession extends ShellSession {
data: `Error occurred: ${get(error, "response.body.message", error ? String(error) : "unknown error")}`, 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(); const env = await this.getCachedShellEnv();

View File

@ -23,8 +23,8 @@ import { stat } from "fs/promises";
import { getOrInsertWith } from "../../common/utils"; import { getOrInsertWith } from "../../common/utils";
export class ShellOpenError extends Error { export class ShellOpenError extends Error {
constructor(message: string, public cause: unknown) { constructor(message: string, options?: ErrorOptions) {
super(`${message}: ${cause}`); super(`${message}`, options);
this.name = this.constructor.name; this.name = this.constructor.name;
Error.captureStackTrace(this); Error.captureStackTrace(this);
} }

View File

@ -94,20 +94,6 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
return undefined; return undefined;
} }
private formatQuotaOptionLabel = (quota: string) => {
const iconMaterial = this.getQuotaOptionLabelIconMaterial(quota);
return iconMaterial
? (
<span className="nobr">
<Icon material={iconMaterial} />
{" "}
{quota}
</span>
)
: quota;
};
setQuota = () => { setQuota = () => {
if (!this.quotaSelectValue) return; if (!this.quotaSelectValue) return;
this.quotas[this.quotaSelectValue] = this.quotaInputValue; this.quotas[this.quotaSelectValue] = this.quotaInputValue;
@ -194,7 +180,7 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
placeholder="Namespace" placeholder="Namespace"
themeName="light" themeName="light"
className="box grow" className="box grow"
onChange={value => this.namespace = value} onChange={option => this.namespace = option?.namespace ?? null}
/> />
<SubTitle title="Values" /> <SubTitle title="Values" />
@ -204,11 +190,27 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
className="quota-select" className="quota-select"
themeName="light" themeName="light"
placeholder="Select a quota.." placeholder="Select a quota.."
options={Object.keys(this.quotas)} options={Object.keys(this.quotas).map(quota => ({ quota }))}
isMulti={false} isMulti={false}
value={this.quotaSelectValue} value={(
onChange={value => this.quotaSelectValue = value} this.quotaSelectValue
formatOptionLabel={this.formatQuotaOptionLabel} ? ({ quota: this.quotaSelectValue })
: null
)}
onChange={option => this.quotaSelectValue = option?.quota ?? null}
formatOptionLabel={({ quota }) => {
const iconMaterial = this.getQuotaOptionLabelIconMaterial(quota);
return iconMaterial
? (
<span className="nobr">
<Icon material={iconMaterial} />
{" "}
{quota}
</span>
)
: quota;
}}
/> />
<Input <Input
maxLength={10} maxLength={10}

View File

@ -18,7 +18,7 @@ import { SubTitle } from "../layout/sub-title";
import { NamespaceSelect } from "../+namespaces/namespace-select"; import { NamespaceSelect } from "../+namespaces/namespace-select";
import { Select } from "../select"; import { Select } from "../select";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { base64, iter } from "../../utils"; import { base64, iter, object } from "../../utils";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import upperFirst from "lodash/upperFirst"; import upperFirst from "lodash/upperFirst";
import { showDetails } from "../kube-detail-params"; import { showDetails } from "../kube-detail-params";
@ -27,20 +27,20 @@ import { fromEntries } from "../../../common/utils/objects";
export interface AddSecretDialogProps extends Partial<DialogProps> { export interface AddSecretDialogProps extends Partial<DialogProps> {
} }
interface ISecretTemplateField { interface SecretTemplateField {
key: string; key: string;
value?: string; value?: string;
required?: boolean; required?: boolean;
} }
interface ISecretTemplate { interface SecretTemplate {
[field: string]: ISecretTemplateField[] | undefined; [field: string]: SecretTemplateField[] | undefined;
annotations?: ISecretTemplateField[]; annotations?: SecretTemplateField[];
labels?: ISecretTemplateField[]; labels?: SecretTemplateField[];
data?: ISecretTemplateField[]; data?: SecretTemplateField[];
} }
type ISecretField = keyof ISecretTemplate; type ISecretField = keyof SecretTemplate;
const dialogState = observable.object({ const dialogState = observable.object({
isOpen: false, isOpen: false,
@ -61,7 +61,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
dialogState.isOpen = false; dialogState.isOpen = false;
} }
private secretTemplate: Partial<Record<SecretType, ISecretTemplate>> = { private secretTemplate: Partial<Record<SecretType, SecretTemplate>> = {
[SecretType.Opaque]: {}, [SecretType.Opaque]: {},
[SecretType.ServiceAccountToken]: { [SecretType.ServiceAccountToken]: {
annotations: [ annotations: [
@ -71,8 +71,8 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
}, },
}; };
get types() { private get secretTypeOptions() {
return Object.keys(this.secretTemplate) as SecretType[]; return object.keys(this.secretTemplate).map(type => ({ type }));
} }
@observable secret = this.secretTemplate; @observable secret = this.secretTemplate;
@ -89,7 +89,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
AddSecretDialog.close(); 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) return iter.pipeline(fields)
.filterMap(({ key, value }) => ( .filterMap(({ key, value }) => (
value value
@ -160,7 +160,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
tabIndex={item.required ? -1 : 0} tabIndex={item.required ? -1 : 0}
readOnly={item.required} readOnly={item.required}
value={item.key} value={item.key}
onChange={v => item.key = v} onChange={v => item.key = v}
/> />
<Input <Input
multiLine multiLine
@ -169,7 +169,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
className="value" className="value"
placeholder="Value" placeholder="Value"
value={item.value} value={item.value}
onChange={v => item.value = v} onChange={v => item.value = v}
/> />
<Icon <Icon
small small
@ -177,7 +177,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
tooltip={item.required ? "Required field" : "Remove field"} tooltip={item.required ? "Required field" : "Remove field"}
className="remove-icon" className="remove-icon"
material="remove_circle_outline" material="remove_circle_outline"
onClick={() => this.removeField(field, index)} onClick={() => this.removeField(field, index)}
/> />
</div> </div>
))} ))}
@ -224,7 +224,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
id="secret-namespace-input" id="secret-namespace-input"
themeName="light" themeName="light"
value={namespace} value={namespace}
onChange={value => this.namespace = value ?? "default"} onChange={value => this.namespace = value?.namespace ?? "default"}
/> />
</div> </div>
<div className="secret-type"> <div className="secret-type">
@ -232,9 +232,9 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
<Select <Select
id="secret-input" id="secret-input"
themeName="light" themeName="light"
options={this.types} options={this.secretTypeOptions}
value={type} value={({ type })}
onChange={value => this.type = value ?? SecretType.Opaque} onChange={value => this.type = value?.type ?? SecretType.Opaque}
/> />
</div> </div>
</div> </div>

View File

@ -188,7 +188,7 @@ class NonInjectedEvents extends React.Component<Dependencies & EventsProps> {
return [ return [
type, // type of event: "Normal" or "Warning" type, // type of event: "Normal" or "Warning"
{ {
className: { warning: isWarning }, className: cssNames({ warning: isWarning }),
title: ( title: (
<> <>
<span id={tooltipId}>{message}</span> <span id={tooltipId}>{message}</span>
@ -209,9 +209,7 @@ class NonInjectedEvents extends React.Component<Dependencies & EventsProps> {
event.getSource(), event.getSource(),
event.count, event.count,
<KubeObjectAge key="age" object={event} />, <KubeObjectAge key="age" object={event} />,
event.lastTimestamp <ReactiveDuration key="last-seen" timestamp={event.lastTimestamp} />,
? <ReactiveDuration key="last-seen" timestamp={event.lastTimestamp} />
: "<unknown>",
]; ];
}} }}
/> />

View File

@ -31,9 +31,21 @@ interface Dependencies {
themeStore: ThemeStore; themeStore: ThemeStore;
} }
const timezoneOptions = moment.tz.names().map(timezone => ({ timezone }));
const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, userStore, themeStore }) => { const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, userStore, themeStore }) => {
const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || ""); const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
const extensionSettings = appPreferenceItems.get().filter((preference) => preference.showInPreferencesTab === "application"); const extensionSettings = appPreferenceItems.get().filter((preference) => preference.showInPreferencesTab === "application");
const themeOptions = [
"system",
...themeStore.themes.keys(),
].map(theme => ({ theme }));
const extensionInstallRegistryOptions = ([
"default",
"npmrc",
"custom",
] as const).map(registry => ({ registry }));
const updateChannelOptions = [...updateChannels.keys()].map(channel => ({ channel }));
return ( return (
<Preferences data-testid="application-preferences-page"> <Preferences data-testid="application-preferences-page">
@ -42,13 +54,10 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
<section id="appearance"> <section id="appearance">
<SubTitle title="Theme" /> <SubTitle title="Theme" />
<Select <Select
options={[ options={themeOptions}
"system", getOptionLabel={option => themeStore.themes.get(option.theme)?.name ?? "Sync with computer"}
...themeStore.themes.keys(), value={({ theme: userStore.colorTheme })}
]} onChange={value => userStore.colorTheme = value?.theme ?? defaultTheme}
getOptionLabel={option => themeStore.themes.get(option)?.name ?? "Sync with computer"}
value={userStore.colorTheme}
onChange={value => userStore.colorTheme = value ?? defaultTheme}
themeName="lens" themeName="lens"
/> />
</section> </section>
@ -58,14 +67,10 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
<section id="extensionRegistryUrl"> <section id="extensionRegistryUrl">
<SubTitle title="Extension Install Registry" /> <SubTitle title="Extension Install Registry" />
<Select <Select
options={[ options={extensionInstallRegistryOptions}
"default", value={({ registry: userStore.extensionRegistryUrl.location })}
"npmrc",
"custom",
]}
value={userStore.extensionRegistryUrl.location}
getOptionLabel={option => { getOptionLabel={option => {
switch (option) { switch (option.registry) {
case "custom": case "custom":
return "Custom Url"; return "Custom Url";
case "default": case "default":
@ -75,7 +80,7 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
} }
}} }}
onChange={value => runInAction(() => { onChange={value => runInAction(() => {
userStore.extensionRegistryUrl.location = value ?? defaultExtensionRegistryUrlLocation; userStore.extensionRegistryUrl.location = value?.registry ?? defaultExtensionRegistryUrlLocation;
if (userStore.extensionRegistryUrl.location === "custom") { if (userStore.extensionRegistryUrl.location === "custom") {
userStore.extensionRegistryUrl.customUrl = ""; userStore.extensionRegistryUrl.customUrl = "";
@ -123,9 +128,9 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
<section id="update-channel"> <section id="update-channel">
<SubTitle title="Update Channel" /> <SubTitle title="Update Channel" />
<Select <Select
options={[...updateChannels.keys()]} options={updateChannelOptions}
value={userStore.updateChannel} value={({ channel: userStore.updateChannel })}
onChange={value => userStore.updateChannel = value ?? defaultUpdateChannel} onChange={value => userStore.updateChannel = value?.channel ?? defaultUpdateChannel}
themeName="lens" themeName="lens"
/> />
</section> </section>
@ -135,10 +140,10 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
<section id="locale"> <section id="locale">
<SubTitle title="Locale Timezone" /> <SubTitle title="Locale Timezone" />
<Select <Select
options={moment.tz.names()} options={timezoneOptions}
value={userStore.localeTimezone} value={({ timezone: userStore.localeTimezone })}
getOptionLabel={timeZone => moment.tz.zone(timeZone)?.name ?? "<unknown>"} getOptionLabel={option => option.timezone}
onChange={value => userStore.localeTimezone = value ?? defaultLocaleTimezone} onChange={value => userStore.localeTimezone = value?.timezone ?? defaultLocaleTimezone}
themeName="lens" themeName="lens"
/> />
</section> </section>

View File

@ -22,6 +22,14 @@ interface Dependencies {
const NonInjectedEditor = observer(({ userStore }: Dependencies) => { const NonInjectedEditor = observer(({ userStore }: Dependencies) => {
const editorConfiguration = userStore.editorConfiguration; const editorConfiguration = userStore.editorConfiguration;
const minimapPositionOptions = (["left", "right"] as const)
.map(side => ({ side }));
const lineNumberOptions = ([
"on",
"off",
"relative",
"interval",
] as const).map(lineNumbers => ({ lineNumbers }));
return ( return (
<Preferences data-testid="editor-preferences-page"> <Preferences data-testid="editor-preferences-page">
@ -43,9 +51,9 @@ const NonInjectedEditor = observer(({ userStore }: Dependencies) => {
<SubHeader compact>Position</SubHeader> <SubHeader compact>Position</SubHeader>
<Select <Select
themeName="lens" themeName="lens"
options={["left", "right"]} options={minimapPositionOptions}
value={editorConfiguration.minimap.side} value={editorConfiguration.minimap.side ? ({ side: editorConfiguration.minimap.side }) : null}
onChange={value => editorConfiguration.minimap.side = value ?? undefined} onChange={value => editorConfiguration.minimap.side = value?.side}
/> />
</div> </div>
</div> </div>
@ -54,15 +62,10 @@ const NonInjectedEditor = observer(({ userStore }: Dependencies) => {
<section> <section>
<SubTitle title="Line numbers"/> <SubTitle title="Line numbers"/>
<Select <Select
options={[ options={lineNumberOptions}
"on", getOptionLabel={option => string.uppercaseFirst(option.lineNumbers)}
"off", value={{ lineNumbers: editorConfiguration.lineNumbers }}
"relative", onChange={value => editorConfiguration.lineNumbers = value?.lineNumbers ?? defaultEditorConfig.lineNumbers}
"interval",
]}
getOptionLabel={string.uppercaseFirst}
value={editorConfiguration.lineNumbers}
onChange={value => editorConfiguration.lineNumbers = value ?? defaultEditorConfig.lineNumbers}
themeName="lens" themeName="lens"
/> />
</section> </section>

View File

@ -30,6 +30,7 @@ const NonInjectedKubectlBinaries= observer(({
const [downloadPath, setDownloadPath] = useState(userStore.downloadBinariesPath || ""); const [downloadPath, setDownloadPath] = useState(userStore.downloadBinariesPath || "");
const [binariesPath, setBinariesPath] = useState(userStore.kubectlBinariesPath || ""); const [binariesPath, setBinariesPath] = useState(userStore.kubectlBinariesPath || "");
const pathValidator = downloadPath ? InputValidators.isPath : undefined; const pathValidator = downloadPath ? InputValidators.isPath : undefined;
const downloadMirrorOptions = [...packageMirrors].map(([name, mirror]) => ({ name, mirror }));
const save = () => { const save = () => {
userStore.downloadBinariesPath = downloadPath; userStore.downloadBinariesPath = downloadPath;
@ -52,12 +53,12 @@ const NonInjectedKubectlBinaries= observer(({
<SubTitle title="Download mirror" /> <SubTitle title="Download mirror" />
<Select <Select
placeholder="Download mirror for kubectl" placeholder="Download mirror for kubectl"
options={[...packageMirrors.keys()]} options={downloadMirrorOptions}
value={userStore.downloadMirror} value={downloadMirrorOptions.find(opt => opt.name === userStore.downloadMirror)}
onChange={value => userStore.downloadMirror = value ?? defaultPackageMirror} onChange={option => userStore.downloadMirror = option?.name ?? defaultPackageMirror}
getOptionLabel={mirrorKey => packageMirrors.get(mirrorKey)?.label ?? "<unknown>"} getOptionLabel={option => option.mirror.label}
isDisabled={!userStore.downloadKubectlBinaries} isDisabled={!userStore.downloadKubectlBinaries}
isOptionDisabled={mirrorKey => !packageMirrors.get(mirrorKey)?.platforms.has(process.platform)} isOptionDisabled={option => option.mirror.platforms.has(process.platform)}
themeName="lens" themeName="lens"
/> />
</section> </section>

View File

@ -24,6 +24,11 @@ interface Dependencies {
} }
const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: Dependencies) => { const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: Dependencies) => {
const themeOptions = [
"",
...themeStore.themes.keys(),
].map(name => ({ name }));
return ( return (
<Preferences data-testid="terminal-preferences-page"> <Preferences data-testid="terminal-preferences-page">
<section> <section>
@ -53,12 +58,9 @@ const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: D
<SubTitle title="Terminal theme" /> <SubTitle title="Terminal theme" />
<Select <Select
themeName="lens" themeName="lens"
options={[ options={themeOptions}
"", getOptionLabel={option => {
...themeStore.themes.keys(), const theme = themeStore.themes.get(option.name);
]}
getOptionLabel={themeName => {
const theme = themeStore.themes.get(themeName);
if (theme) { if (theme) {
return theme.name; return theme.name;
@ -66,8 +68,8 @@ const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: D
return "Match System Theme"; return "Match System Theme";
}} }}
value={userStore.terminalTheme} value={{ name: userStore.terminalTheme }}
onChange={value => userStore.terminalTheme = value ?? ""} onChange={option => userStore.terminalTheme = option?.name ?? ""}
/> />
</section> </section>

View File

@ -11,8 +11,8 @@ import { KubeObjectListLayout } from "../../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { ClusterRoleBindingDialog } from "./dialog"; import { ClusterRoleBindingDialog } from "./dialog";
import { clusterRoleBindingStore } from "./legacy-store"; import { clusterRoleBindingStore } from "./legacy-store";
import { clusterRolesStore } from "../+cluster-roles/store"; import { clusterRoleStore } from "../+cluster-roles/legacy-store";
import { serviceAccountsStore } from "../+service-accounts/store"; import { serviceAccountStore } from "../+service-accounts/legacy-store";
import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout"; import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout";
import { KubeObjectAge } from "../../kube-object/age"; import { KubeObjectAge } from "../../kube-object/age";
@ -33,7 +33,7 @@ export class ClusterRoleBindings extends React.Component {
tableId="access_cluster_role_bindings" tableId="access_cluster_role_bindings"
className="ClusterRoleBindings" className="ClusterRoleBindings"
store={clusterRoleBindingStore} store={clusterRoleBindingStore}
dependentStores={[clusterRolesStore, serviceAccountsStore]} dependentStores={[clusterRoleStore, serviceAccountStore]}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: binding => binding.getName(), [columnId.name]: binding => binding.getName(),
[columnId.bindings]: binding => binding.getSubjectNames(), [columnId.bindings]: binding => binding.getSubjectNames(),

View File

@ -10,9 +10,9 @@ import { KubeObjectListLayout } from "../../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { RoleBindingDialog } from "./dialog"; import { RoleBindingDialog } from "./dialog";
import { roleBindingStore } from "./store"; import { roleBindingStore } from "./store";
import { rolesStore } from "../+roles/store"; import { roleStore } from "../+roles/legacy-store";
import { clusterRolesStore } from "../+cluster-roles/store"; import { clusterRoleStore } from "../+cluster-roles/legacy-store";
import { serviceAccountsStore } from "../+service-accounts/store"; import { serviceAccountStore } from "../+service-accounts/legacy-store";
import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout"; import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout";
import { KubeObjectAge } from "../../kube-object/age"; import { KubeObjectAge } from "../../kube-object/age";
@ -33,7 +33,7 @@ export class RoleBindings extends React.Component {
tableId="access_role_bindings" tableId="access_role_bindings"
className="RoleBindings" className="RoleBindings"
store={roleBindingStore} store={roleBindingStore}
dependentStores={[rolesStore, clusterRolesStore, serviceAccountsStore]} dependentStores={[roleStore, clusterRoleStore, serviceAccountStore]}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: binding => binding.getName(), [columnId.name]: binding => binding.getName(),
[columnId.namespace]: binding => binding.getNs(), [columnId.namespace]: binding => binding.getNs(),

View File

@ -17,7 +17,7 @@ import { showDetails } from "../../kube-detail-params";
import { SubTitle } from "../../layout/sub-title"; import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications"; import { Notifications } from "../../notifications";
import { Wizard, WizardStep } from "../../wizard"; import { Wizard, WizardStep } from "../../wizard";
import { rolesStore } from "./store"; import { roleStore } from "./legacy-store";
export interface AddRoleDialogProps extends Partial<DialogProps> { export interface AddRoleDialogProps extends Partial<DialogProps> {
} }
@ -49,7 +49,7 @@ export class AddRoleDialog extends React.Component<AddRoleDialogProps> {
createRole = async () => { createRole = async () => {
try { try {
const role = await rolesStore.create({ name: this.roleName, namespace: this.namespace }); const role = await roleStore.create({ name: this.roleName, namespace: this.namespace });
showDetails(role.selfLink); showDetails(role.selfLink);
this.reset(); this.reset();
@ -90,7 +90,7 @@ export class AddRoleDialog extends React.Component<AddRoleDialogProps> {
id="add-dialog-namespace-select-input" id="add-dialog-namespace-select-input"
themeName="light" themeName="light"
value={this.namespace} value={this.namespace}
onChange={namespace => this.namespace = namespace ?? "default"} onChange={option => this.namespace = option?.namespace ?? "default"}
/> />
</WizardStep> </WizardStep>
</Wizard> </Wizard>

View File

@ -7,7 +7,8 @@ import "./welcome.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import Carousel from "react-material-ui-carousel"; import type { CarouselProps } from "react-material-ui-carousel";
import LegacyCarousel from "react-material-ui-carousel";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { productName, slackUrl } from "../../../common/vars"; import { productName, slackUrl } from "../../../common/vars";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
@ -18,6 +19,9 @@ import type { WelcomeBannerRegistration } from "./welcome-banner-items/welcome-b
export const defaultWidth = 320; export const defaultWidth = 320;
// This is to fix some react 18 type errors
const Carousel = LegacyCarousel as React.ComponentType<CarouselProps>;
interface Dependencies { interface Dependencies {
welcomeMenuItems: IComputedValue<WelcomeMenuRegistration[]>; welcomeMenuItems: IComputedValue<WelcomeMenuRegistration[]>;
welcomeBannerItems: IComputedValue<WelcomeBannerRegistration[]>; welcomeBannerItems: IComputedValue<WelcomeBannerRegistration[]>;

View File

@ -10,11 +10,11 @@ import capitalize from "lodash/capitalize";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { DatasetTooltipLabel, PieChartData } from "../chart"; import type { DatasetTooltipLabel, PieChartData } from "../chart";
import { PieChart } from "../chart"; import { PieChart } from "../chart";
import { cssVar } from "../../utils"; import { cssVar, object } from "../../utils";
import { ThemeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
export interface OverviewWorkloadStatusProps { export interface OverviewWorkloadStatusProps {
status: Record<string, number>; status: Partial<Record<string, number>>;
} }
@observer @observer
@ -32,7 +32,7 @@ export class OverviewWorkloadStatus extends React.Component<OverviewWorkloadStat
datasets: [], datasets: [],
}; };
const statuses = Object.entries(this.props.status).filter(([, val]) => val > 0); const statuses = object.entries(this.props.status).filter(([, val]) => val > 0);
if (statuses.length === 0) { if (statuses.length === 0) {
chartData.datasets.push({ chartData.datasets.push({

View File

@ -10,7 +10,7 @@ import { Icon } from "../../icon/icon";
import { Button } from "../../button/button"; import { Button } from "../../button/button";
import { SubTitle } from "../../layout/sub-title"; import { SubTitle } from "../../layout/sub-title";
import type { Cluster } from "../../../../common/cluster/cluster"; import type { Cluster } from "../../../../common/cluster/cluster";
import { observable, reaction, makeObservable } from "mobx"; import { observable, reaction, makeObservable, runInAction } from "mobx";
import { ClusterMetricsResourceType } from "../../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../../common/cluster-types";
export interface ClusterMetricsSettingProps { export interface ClusterMetricsSettingProps {
@ -40,21 +40,8 @@ export class ClusterMetricsSetting extends React.Component<ClusterMetricsSetting
this.props.cluster.preferences.hiddenMetrics = Array.from(this.hiddenMetrics); this.props.cluster.preferences.hiddenMetrics = Array.from(this.hiddenMetrics);
}; };
onChangeSelect = (values: readonly ClusterMetricsResourceType[]) => {
for (const value of values) {
if (this.hiddenMetrics.has(value)) {
this.hiddenMetrics.delete(value);
} else {
this.hiddenMetrics.add(value);
}
}
this.save();
};
onChangeButton = () => { onChangeButton = () => {
Object.keys(ClusterMetricsResourceType).map(value => this.hiddenMetrics.replace(Object.keys(ClusterMetricsResourceType));
this.hiddenMetrics.add(value),
);
this.save(); this.save();
}; };
@ -63,20 +50,9 @@ export class ClusterMetricsSetting extends React.Component<ClusterMetricsSetting
this.save(); this.save();
}; };
formatOptionLabel = (resource: ClusterMetricsResourceType) => (
<div className="flex gaps align-center">
<span>{resource}</span>
{this.hiddenMetrics.has(resource) && (
<Icon
smallest
material="check"
className="box right"
/>
)}
</div>
);
renderMetricsSelect() { renderMetricsSelect() {
const metricResourceTypeOptions = Object.values(ClusterMetricsResourceType)
.map(type => ({ type }));
return ( return (
<> <>
@ -89,9 +65,26 @@ export class ClusterMetricsSetting extends React.Component<ClusterMetricsSetting
onMenuClose={this.save} onMenuClose={this.save}
closeMenuOnSelect={false} closeMenuOnSelect={false}
controlShouldRenderValue={false} controlShouldRenderValue={false}
options={Object.values(ClusterMetricsResourceType)} options={metricResourceTypeOptions}
onChange={this.onChangeSelect} onChange={(options) => {
formatOptionLabel={this.formatOptionLabel} runInAction(() => {
this.hiddenMetrics.replace(options.map(opt => opt.type));
this.save();
});
}}
getOptionLabel={option => option.type}
formatOptionLabel={(option) => (
<div className="flex gaps align-center">
<span>{option.type}</span>
{this.hiddenMetrics.has(option.type) && (
<Icon
smallest
material="check"
className="box right"
/>
)}
</div>
)}
themeName="lens" themeName="lens"
/> />
<Button <Button

View File

@ -22,20 +22,23 @@ export interface ClusterPrometheusSettingProps {
const autoDetectPrometheus = Symbol("auto-detect-prometheus"); const autoDetectPrometheus = Symbol("auto-detect-prometheus");
type ProviderOption = typeof autoDetectPrometheus | string; interface ProviderOption {
provider: typeof autoDetectPrometheus | string;
}
@observer @observer
export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusSettingProps> { export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusSettingProps> {
@observable path = ""; @observable path = "";
@observable provider: ProviderOption = autoDetectPrometheus; @observable selectedOption = this.options.find(opt => opt.provider === autoDetectPrometheus);
@observable loading = true; @observable loading = true;
loadedOptions = observable.map<string, MetricProviderInfo>(); loadedOptions = observable.map<string, MetricProviderInfo>();
@computed get options(): ProviderOption[] { @computed get options(): ProviderOption[] {
return [ return ([
autoDetectPrometheus, autoDetectPrometheus,
...this.loadedOptions.keys(), ...this.loadedOptions.keys(),
]; ] as const)
.map(provider => ({ provider }));
} }
constructor(props: ClusterPrometheusSettingProps) { constructor(props: ClusterPrometheusSettingProps) {
@ -44,11 +47,11 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
} }
@computed get canEditPrometheusPath(): boolean { @computed get canEditPrometheusPath(): boolean {
if (this.provider === autoDetectPrometheus) { if (!this.selectedOption || this.selectedOption.provider === autoDetectPrometheus) {
return false; return false;
} }
return this.loadedOptions.get(this.provider)?.isConfigurable ?? false; return this.loadedOptions.get(this.selectedOption.provider)?.isConfigurable ?? false;
} }
componentDidMount() { componentDidMount() {
@ -65,9 +68,9 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
} }
if (prometheusProvider) { if (prometheusProvider) {
this.provider = prometheusProvider.type; this.selectedOption = this.options.find(opt => opt.provider === prometheusProvider.type);
} else { } else {
this.provider = ""; this.selectedOption = undefined;
} }
}), }),
); );
@ -81,7 +84,7 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
} }
parsePrometheusPath = () => { parsePrometheusPath = () => {
if (!this.provider || !this.path) { if (!this.selectedOption || !this.path) {
return undefined; return undefined;
} }
const parsed = this.path.split(/\/|:/, 3); const parsed = this.path.split(/\/|:/, 3);
@ -100,8 +103,8 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
}; };
onSaveProvider = () => { onSaveProvider = () => {
this.props.cluster.preferences.prometheusProvider = typeof this.provider === "string" this.props.cluster.preferences.prometheusProvider = typeof this.selectedOption === "string"
? { type: this.provider } ? { type: this.selectedOption }
: undefined; : undefined;
}; };
@ -121,9 +124,9 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
<> <>
<Select<ProviderOption, false, GroupBase<ProviderOption>> <Select<ProviderOption, false, GroupBase<ProviderOption>>
id="cluster-prometheus-settings-input" id="cluster-prometheus-settings-input"
value={this.provider} value={this.selectedOption}
onChange={provider => { onChange={provider => {
this.provider = provider ?? autoDetectPrometheus; this.selectedOption = provider ?? { provider: autoDetectPrometheus };
this.onSaveProvider(); this.onSaveProvider();
}} }}
options={this.options} options={this.options}

View File

@ -18,6 +18,7 @@ import { iter } from "../../utils";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import registeredCommandsInjectable from "./registered-commands/registered-commands.injectable"; import registeredCommandsInjectable from "./registered-commands/registered-commands.injectable";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import type { SingleValue } from "react-select";
interface Dependencies { interface Dependencies {
commands: IComputedValue<Map<string, RegisteredCommand>>; commands: IComputedValue<Map<string, RegisteredCommand>>;
@ -28,20 +29,14 @@ interface Dependencies {
const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeCommandOverlay }: Dependencies) => { const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeCommandOverlay }: Dependencies) => {
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
const executeAction = (commandId: string | null) => { const executeAction = (option: SingleValue<typeof activeCommands[number]>) => {
if (!commandId) { if (!option) {
return;
}
const command = commands.get().get(commandId);
if (!command) {
return; return;
} }
try { try {
closeCommandOverlay(); closeCommandOverlay();
command.action({ option.action({
entity: activeEntity, entity: activeEntity,
navigate: (url, opts = {}) => { navigate: (url, opts = {}) => {
const { forceRootFrame = false } = opts; const { forceRootFrame = false } = opts;
@ -54,7 +49,7 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman
}, },
}); });
} catch (error) { } catch (error) {
console.error("[COMMAND-DIALOG] failed to execute command", command.id, error); console.error("[COMMAND-DIALOG] failed to execute command", option.id, error);
} }
}; };
@ -70,8 +65,7 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman
return void console.error(`[COMMAND-DIALOG]: isActive for ${command.id} threw an error, defaulting to false`, error); return void console.error(`[COMMAND-DIALOG]: isActive for ${command.id} threw an error, defaulting to false`, error);
} }
}) })
.map(({ id, ...rest }) => [id, rest] as const) .collect(items => Array.from(items));
.collect(items => new Map(items));
return ( return (
<Select <Select
@ -83,15 +77,12 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman
IndicatorSeparator: null, IndicatorSeparator: null,
}} }}
menuIsOpen menuIsOpen
options={Array.from(activeCommands.keys())} options={activeCommands}
getOptionLabel={commandId => { getOptionLabel={({ title }) => (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion typeof title === "string"
const { title } = activeCommands.get(commandId)!;
return typeof title === "string"
? title ? title
: title(context); : title(context)
}} )}
autoFocus={true} autoFocus={true}
escapeClearsValue={false} escapeClearsValue={false}
data-test-id="command-palette-search" data-test-id="command-palette-search"

View File

@ -9,7 +9,7 @@ import React from "react";
import { formatDuration } from "../../utils"; import { formatDuration } from "../../utils";
export interface ReactiveDurationProps { export interface ReactiveDurationProps {
timestamp: string; timestamp: string | undefined;
/** /**
* Whether the display string should prefer length over precision * 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) => { export const ReactiveDuration = observer(({ timestamp, compact = true }: ReactiveDurationProps) => {
if (!timestamp) {
return <>{"<unknown>"}</>;
}
const creationTimestamp = new Date(timestamp).getTime(); const creationTimestamp = new Date(timestamp).getTime();
return ( return (

View File

@ -21,12 +21,8 @@ export interface KubeObjectAgeProps {
} }
export const KubeObjectAge = ({ object, compact = true }: KubeObjectAgeProps) => ( export const KubeObjectAge = ({ object, compact = true }: KubeObjectAgeProps) => (
object.metadata.creationTimestamp <ReactiveDuration
? ( timestamp={object.metadata.creationTimestamp}
<ReactiveDuration compact={compact}
timestamp={object.metadata.creationTimestamp} />
compact={compact}
/>
)
: <>{"<unknown>"}</>
); );

View File

@ -5,12 +5,14 @@
import "./sub-header.scss"; import "./sub-header.scss";
import React from "react"; import React from "react";
import type { SingleOrMany } from "../../utils";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
export interface SubHeaderProps { export interface SubHeaderProps {
className?: string; className?: string;
withLine?: boolean; // add bottom line withLine?: boolean; // add bottom line
compact?: boolean; // no extra padding around content compact?: boolean; // no extra padding around content
children: SingleOrMany<React.ReactNode>;
} }
export class SubHeader extends React.Component<SubHeaderProps> { export class SubHeader extends React.Component<SubHeaderProps> {