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

Fix test flakiness by removing side effects from userStore preferences

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-12-06 17:14:30 -05:00
parent a6d1b7a3a4
commit f95ed19a7d
24 changed files with 369 additions and 385 deletions

View File

@ -4,7 +4,7 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { Stats } from "fs"; import type { Stats } from "fs";
import fsInjectable from "../fs.injectable"; import fsInjectable from "./fs.injectable";
export type Stat = (path: string) => Promise<Stats>; export type Stat = (path: string) => Promise<Stats>;

View File

@ -1,10 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import statInjectable from "./stat.injectable";
import { getGlobalOverride } from "../../test-utils/get-global-override";
export default getGlobalOverride(statInjectable, () => () => {
throw new Error("Tried to call stat without explicit override");
});

View File

@ -7,7 +7,7 @@ import type { AsyncResult } from "../utils/async-result";
import { isErrnoException } from "../utils"; import { isErrnoException } from "../utils";
import type { Stats } from "fs-extra"; import type { Stats } from "fs-extra";
import { lowerFirst } from "lodash/fp"; import { lowerFirst } from "lodash/fp";
import statInjectable from "./stat/stat.injectable"; import statInjectable from "./stat.injectable";
export type ValidateDirectory = (path: string) => Promise<AsyncResult<undefined>>; export type ValidateDirectory = (path: string) => Promise<AsyncResult<undefined>>;

View File

@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import currentTimezoneInjectable from "./current-timezone.injectable";
export default getGlobalOverride(currentTimezoneInjectable, () => "Etc/GMT");

View File

@ -0,0 +1,14 @@
/**
* 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 moment from "moment";
const currentTimezoneInjectable = getInjectable({
id: "current-timezone",
instantiate: () => moment.tz.guess(true),
causesSideEffects: true,
});
export default currentTimezoneInjectable;

View File

@ -0,0 +1,141 @@
/**
* 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 { merge } from "lodash";
import homeDirectoryPathInjectable from "../os/home-directory-path.injectable";
import joinPathsInjectable from "../path/join-paths.injectable";
import { defaultThemeId } from "../vars";
import currentTimezoneInjectable from "./current-timezone.injectable";
import type { EditorConfiguration, ExtensionRegistry, KubeconfigSyncEntry, KubeconfigSyncValue, TerminalConfig } from "./preferences-helpers";
import { defaultExtensionRegistryUrlLocation, defaultEditorConfig, defaultTerminalConfig, defaultPackageMirror, getPreferenceDescriptor, packageMirrors } from "./preferences-helpers";
export type PreferenceDescriptors = ReturnType<typeof userStorePreferenceDescriptorsInjectable["instantiate"]>;
const userStorePreferenceDescriptorsInjectable = getInjectable({
id: "user-store-preference-descriptors",
instantiate: (di) => {
const currentTimezone = di.inject(currentTimezoneInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const homeDirectoryPath = di.inject(homeDirectoryPathInjectable);
const mainKubeFolderPath = joinPaths(homeDirectoryPath, ".kube");
return ({
httpsProxy: getPreferenceDescriptor<string | undefined>({
fromStore: val => val,
toStore: val => val || undefined,
}),
shell: getPreferenceDescriptor<string | undefined>({
fromStore: val => val,
toStore: val => val || undefined,
}),
colorTheme: getPreferenceDescriptor<string>({
fromStore: val => val || defaultThemeId,
toStore: val => !val || val === defaultThemeId
? undefined
: val,
}),
terminalTheme: getPreferenceDescriptor<string>({
fromStore: val => val || "",
toStore: val => val || undefined,
}),
localeTimezone: getPreferenceDescriptor<string>({
fromStore: val => val || currentTimezone,
toStore: val => !val || val === currentTimezone
? undefined
: val,
}),
allowUntrustedCAs: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? false,
toStore: val => !val
? undefined
: val,
}),
allowErrorReporting: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? true,
toStore: val => val
? undefined
: val,
}),
downloadMirror: getPreferenceDescriptor<string>({
fromStore: val => !val || !packageMirrors.has(val)
? defaultPackageMirror
: val,
toStore: val => val === defaultPackageMirror
? undefined
: val,
}),
downloadKubectlBinaries: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? true,
toStore: val => val
? undefined
: val,
}),
downloadBinariesPath: getPreferenceDescriptor<string | undefined>({
fromStore: val => val,
toStore: val => val || undefined,
}),
kubectlBinariesPath: getPreferenceDescriptor<string | undefined>({
fromStore: val => val,
toStore: val => val || undefined,
}),
openAtLogin: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? false,
toStore: val => !val
? undefined
: val,
}),
terminalCopyOnSelect: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? false,
toStore: val => !val
? undefined
: val,
}),
hiddenTableColumns: getPreferenceDescriptor<[string, string[]][], Map<string, Set<string>>>({
fromStore: (val = []) => new Map(
val.map(([tableId, columnIds]) => [tableId, new Set(columnIds)]),
),
toStore: (val) => {
const res: [string, string[]][] = [];
for (const [table, columns] of val) {
if (columns.size) {
res.push([table, Array.from(columns)]);
}
}
return res.length ? res : undefined;
},
}),
syncKubeconfigEntries: getPreferenceDescriptor<KubeconfigSyncEntry[], Map<string, KubeconfigSyncValue>>({
fromStore: val => new Map(
val?.map(({ filePath, ...rest }) => [filePath, rest])
?? [[mainKubeFolderPath, {}]],
),
toStore: val => val.size === 1 && val.has(mainKubeFolderPath)
? undefined
: Array.from(val, ([filePath, rest]) => ({ filePath, ...rest })),
}),
editorConfiguration: getPreferenceDescriptor<Partial<EditorConfiguration>, EditorConfiguration>({
fromStore: val => merge(defaultEditorConfig, val),
toStore: val => val,
}),
terminalConfig: getPreferenceDescriptor<Partial<TerminalConfig>, TerminalConfig>({
fromStore: val => merge(defaultTerminalConfig, val),
toStore: val => val,
}),
extensionRegistryUrl: getPreferenceDescriptor<ExtensionRegistry>({
fromStore: val => val ?? {
location: defaultExtensionRegistryUrlLocation,
},
toStore: val => val.location === defaultExtensionRegistryUrlLocation
? undefined
: val,
}),
}) as const;
},
});
export default userStorePreferenceDescriptorsInjectable;

View File

@ -3,14 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import moment from "moment-timezone";
import path from "path";
import os from "os";
import type { editor } from "monaco-editor"; import type { editor } from "monaco-editor";
import merge from "lodash/merge"; import { defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars";
import { defaultThemeId, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars"; import type { PreferenceDescriptors } from "./preference-descriptors.injectable";
import type { ObservableMap } from "mobx";
import { observable } from "mobx";
export interface KubeconfigSyncEntry extends KubeconfigSyncValue { export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
filePath: string; filePath: string;
@ -54,86 +49,8 @@ export interface PreferenceDescription<T, R = T> {
toStore(val: R): T | undefined; toStore(val: R): T | undefined;
} }
const httpsProxy: PreferenceDescription<string | undefined> = { export const getPreferenceDescriptor = <T, R = T>(desc: PreferenceDescription<T, R>) => desc;
fromStore(val) {
return val;
},
toStore(val) {
return val || undefined;
},
};
const shell: PreferenceDescription<string | undefined> = {
fromStore(val) {
return val;
},
toStore(val) {
return val || undefined;
},
};
const colorTheme: PreferenceDescription<string> = {
fromStore(val) {
return val || defaultThemeId;
},
toStore(val) {
if (!val || val === defaultThemeId) {
return undefined;
}
return val;
},
};
const terminalTheme: PreferenceDescription<string> = {
fromStore(val) {
return val || "";
},
toStore(val) {
return val || undefined;
},
};
export const defaultLocaleTimezone = "UTC";
const localeTimezone: PreferenceDescription<string> = {
fromStore(val) {
return val || moment.tz.guess(true) || defaultLocaleTimezone;
},
toStore(val) {
if (!val || val === moment.tz.guess(true) || val === defaultLocaleTimezone) {
return undefined;
}
return val;
},
};
const allowUntrustedCAs: PreferenceDescription<boolean> = {
fromStore(val) {
return val ?? false;
},
toStore(val) {
if (!val) {
return undefined;
}
return val;
},
};
const allowErrorReporting: PreferenceDescription<boolean> = {
fromStore(val) {
return val ?? true;
},
toStore(val) {
if (val === true) {
return undefined;
}
return val;
},
};
export interface DownloadMirror { export interface DownloadMirror {
url: string; url: string;
@ -157,142 +74,6 @@ export const packageMirrors = new Map<string, DownloadMirror>([
}], }],
]); ]);
const downloadMirror: PreferenceDescription<string> = {
fromStore(val) {
return !val || !packageMirrors.has(val)
? defaultPackageMirror
: val;
},
toStore(val) {
if (!val || val === defaultPackageMirror) {
return undefined;
}
return val;
},
};
const downloadKubectlBinaries: PreferenceDescription<boolean> = {
fromStore(val) {
return val ?? true;
},
toStore(val) {
if (val === true) {
return undefined;
}
return val;
},
};
const downloadBinariesPath: PreferenceDescription<string | undefined> = {
fromStore(val) {
return val;
},
toStore(val) {
if (!val) {
return undefined;
}
return val;
},
};
const kubectlBinariesPath: PreferenceDescription<string | undefined> = {
fromStore(val) {
return val;
},
toStore(val) {
if (!val) {
return undefined;
}
return val;
},
};
const openAtLogin: PreferenceDescription<boolean> = {
fromStore(val) {
return val ?? false;
},
toStore(val) {
if (!val) {
return undefined;
}
return val;
},
};
const terminalCopyOnSelect: PreferenceDescription<boolean> = {
fromStore(val) {
return val ?? false;
},
toStore(val) {
if (!val) {
return undefined;
}
return val;
},
};
const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string, Set<string>>> = {
fromStore(val) {
return new Map(
(val ?? []).map(([tableId, columnIds]) => [tableId, new Set(columnIds)]),
);
},
toStore(val) {
const res: [string, string[]][] = [];
for (const [table, columns] of val) {
if (columns.size) {
res.push([table, Array.from(columns)]);
}
}
return res.length ? res : undefined;
},
};
const mainKubeFolder = path.join(os.homedir(), ".kube");
const syncKubeconfigEntries: PreferenceDescription<KubeconfigSyncEntry[], ObservableMap<string, KubeconfigSyncValue>> = {
fromStore(val) {
return observable.map(
val
?.map(({ filePath, ...rest }) => [filePath, rest])
?? [[mainKubeFolder, {}]],
);
},
toStore(val) {
if (val.size === 1 && val.has(mainKubeFolder)) {
return undefined;
}
return Array.from(val, ([filePath, rest]) => ({ filePath, ...rest }));
},
};
const editorConfiguration: PreferenceDescription<Partial<EditorConfiguration> | undefined, EditorConfiguration> = {
fromStore(val) {
return merge(defaultEditorConfig, val);
},
toStore(val) {
return val;
},
};
const terminalConfig: PreferenceDescription<TerminalConfig, TerminalConfig> = {
fromStore(val) {
return merge(defaultTerminalConfig, val);
},
toStore(val) {
return val;
},
};
export type ExtensionRegistryLocation = "default" | "npmrc" | "custom"; export type ExtensionRegistryLocation = "default" | "npmrc" | "custom";
export type ExtensionRegistry = { export type ExtensionRegistry = {
@ -306,49 +87,13 @@ export type ExtensionRegistry = {
export const defaultExtensionRegistryUrlLocation = "default"; export const defaultExtensionRegistryUrlLocation = "default";
export const defaultExtensionRegistryUrl = "https://registry.npmjs.org"; export const defaultExtensionRegistryUrl = "https://registry.npmjs.org";
const extensionRegistryUrl: PreferenceDescription<ExtensionRegistry> = { type PreferencesModelType<field extends keyof PreferenceDescriptors> = PreferenceDescriptors[field] extends PreferenceDescription<infer T, any> ? T : never;
fromStore(val) { type UserStoreModelType<field extends keyof PreferenceDescriptors> = PreferenceDescriptors[field] extends PreferenceDescription<any, infer T> ? T : never;
return val ?? {
location: defaultExtensionRegistryUrlLocation,
};
},
toStore(val) {
if (val.location === defaultExtensionRegistryUrlLocation) {
return undefined;
}
return val;
},
};
type PreferencesModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<infer T, any> ? T : never;
type UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never;
export type UserStoreFlatModel = { export type UserStoreFlatModel = {
[field in keyof typeof DESCRIPTORS]: UserStoreModelType<field>; [field in keyof PreferenceDescriptors]: UserStoreModelType<field>;
}; };
export type UserPreferencesModel = { export type UserPreferencesModel = {
[field in keyof typeof DESCRIPTORS]: PreferencesModelType<field>; [field in keyof PreferenceDescriptors]: PreferencesModelType<field>;
} & { updateChannel: string }; } & { updateChannel: string };
export const DESCRIPTORS = {
httpsProxy,
shell,
colorTheme,
terminalTheme,
localeTimezone,
allowUntrustedCAs,
allowErrorReporting,
downloadMirror,
downloadKubectlBinaries,
downloadBinariesPath,
kubectlBinariesPath,
openAtLogin,
hiddenTableColumns,
syncKubeconfigEntries,
editorConfiguration,
terminalCopyOnSelect,
terminalConfig,
extensionRegistryUrl,
};

View File

@ -17,6 +17,7 @@ import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-s
import { persistStateToConfigInjectionToken } from "../base-store/save-to-file"; import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import getBasenameOfPathInjectable from "../path/get-basename.injectable"; import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token"; import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token";
import userStorePreferenceDescriptorsInjectable from "./preference-descriptors.injectable";
const userStoreInjectable = getInjectable({ const userStoreInjectable = getInjectable({
id: "user-store", id: "user-store",
@ -34,6 +35,7 @@ const userStoreInjectable = getInjectable({
persistStateToConfig: di.inject(persistStateToConfigInjectionToken), persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken), enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken), shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
preferenceDescriptors: di.inject(userStorePreferenceDescriptorsInjectable),
}), }),
}); });

View File

@ -7,13 +7,13 @@ import { action, observable, makeObservable, isObservableArray, isObservableSet,
import type { BaseStoreDependencies } from "../base-store/base-store"; import type { BaseStoreDependencies } from "../base-store/base-store";
import { BaseStore } from "../base-store/base-store"; import { BaseStore } from "../base-store/base-store";
import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils"; import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils";
import { DESCRIPTORS } from "./preferences-helpers";
import type { UserPreferencesModel, StoreType } from "./preferences-helpers"; import type { UserPreferencesModel, StoreType } from "./preferences-helpers";
import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable"; import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable";
// TODO: Remove coupling with Feature // TODO: Remove coupling with Feature
import type { SelectedUpdateChannel } from "../../features/application-update/common/selected-update-channel/selected-update-channel.injectable"; import type { SelectedUpdateChannel } from "../../features/application-update/common/selected-update-channel/selected-update-channel.injectable";
import type { ReleaseChannel } from "../../features/application-update/common/update-channels"; import type { ReleaseChannel } from "../../features/application-update/common/update-channels";
import type { PreferenceDescriptors } from "./preference-descriptors.injectable";
export interface UserStoreModel { export interface UserStoreModel {
preferences: UserPreferencesModel; preferences: UserPreferencesModel;
@ -21,6 +21,7 @@ export interface UserStoreModel {
interface Dependencies extends BaseStoreDependencies { interface Dependencies extends BaseStoreDependencies {
readonly selectedUpdateChannel: SelectedUpdateChannel; readonly selectedUpdateChannel: SelectedUpdateChannel;
readonly preferenceDescriptors: PreferenceDescriptors;
emitAppEvent: EmitAppEvent; emitAppEvent: EmitAppEvent;
} }
@ -43,45 +44,45 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
*/ */
@observable newContexts = observable.set<string>(); @observable newContexts = observable.set<string>();
@observable allowErrorReporting!: StoreType<typeof DESCRIPTORS["allowErrorReporting"]>; @observable allowErrorReporting!: StoreType<PreferenceDescriptors["allowErrorReporting"]>;
@observable allowUntrustedCAs!: StoreType<typeof DESCRIPTORS["allowUntrustedCAs"]>; @observable allowUntrustedCAs!: StoreType<PreferenceDescriptors["allowUntrustedCAs"]>;
@observable colorTheme!: StoreType<typeof DESCRIPTORS["colorTheme"]>; @observable colorTheme!: StoreType<PreferenceDescriptors["colorTheme"]>;
@observable terminalTheme!: StoreType<typeof DESCRIPTORS["terminalTheme"]>; @observable terminalTheme!: StoreType<PreferenceDescriptors["terminalTheme"]>;
@observable localeTimezone!: StoreType<typeof DESCRIPTORS["localeTimezone"]>; @observable localeTimezone!: StoreType<PreferenceDescriptors["localeTimezone"]>;
@observable downloadMirror!: StoreType<typeof DESCRIPTORS["downloadMirror"]>; @observable downloadMirror!: StoreType<PreferenceDescriptors["downloadMirror"]>;
@observable httpsProxy!: StoreType<typeof DESCRIPTORS["httpsProxy"]>; @observable httpsProxy!: StoreType<PreferenceDescriptors["httpsProxy"]>;
@observable shell!: StoreType<typeof DESCRIPTORS["shell"]>; @observable shell!: StoreType<PreferenceDescriptors["shell"]>;
@observable downloadBinariesPath!: StoreType<typeof DESCRIPTORS["downloadBinariesPath"]>; @observable downloadBinariesPath!: StoreType<PreferenceDescriptors["downloadBinariesPath"]>;
@observable kubectlBinariesPath!: StoreType<typeof DESCRIPTORS["kubectlBinariesPath"]>; @observable kubectlBinariesPath!: StoreType<PreferenceDescriptors["kubectlBinariesPath"]>;
@observable terminalCopyOnSelect!: StoreType<typeof DESCRIPTORS["terminalCopyOnSelect"]>; @observable terminalCopyOnSelect!: StoreType<PreferenceDescriptors["terminalCopyOnSelect"]>;
@observable terminalConfig!: StoreType<typeof DESCRIPTORS["terminalConfig"]>; @observable terminalConfig!: StoreType<PreferenceDescriptors["terminalConfig"]>;
@observable extensionRegistryUrl!: StoreType<typeof DESCRIPTORS["extensionRegistryUrl"]>; @observable extensionRegistryUrl!: StoreType<PreferenceDescriptors["extensionRegistryUrl"]>;
/** /**
* Download kubectl binaries matching cluster version * Download kubectl binaries matching cluster version
*/ */
@observable downloadKubectlBinaries!: StoreType<typeof DESCRIPTORS["downloadKubectlBinaries"]>; @observable downloadKubectlBinaries!: StoreType<PreferenceDescriptors["downloadKubectlBinaries"]>;
/** /**
* Whether the application should open itself at login. * Whether the application should open itself at login.
*/ */
@observable openAtLogin!: StoreType<typeof DESCRIPTORS["openAtLogin"]>; @observable openAtLogin!: StoreType<PreferenceDescriptors["openAtLogin"]>;
/** /**
* The column IDs under each configurable table ID that have been configured * The column IDs under each configurable table ID that have been configured
* to not be shown * to not be shown
*/ */
@observable hiddenTableColumns!: StoreType<typeof DESCRIPTORS["hiddenTableColumns"]>; @observable hiddenTableColumns!: StoreType<PreferenceDescriptors["hiddenTableColumns"]>;
/** /**
* Monaco editor configs * Monaco editor configs
*/ */
@observable editorConfiguration!: StoreType<typeof DESCRIPTORS["editorConfiguration"]>; @observable editorConfiguration!: StoreType<PreferenceDescriptors["editorConfiguration"]>;
/** /**
* The set of file/folder paths to be synced * The set of file/folder paths to be synced
*/ */
@observable syncKubeconfigEntries!: StoreType<typeof DESCRIPTORS["syncKubeconfigEntries"]>; @observable syncKubeconfigEntries!: StoreType<PreferenceDescriptors["syncKubeconfigEntries"]>;
/** /**
* Checks if a column (by ID) for a table (by ID) is configured to be hidden * Checks if a column (by ID) for a table (by ID) is configured to be hidden
@ -112,14 +113,14 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
@action @action
resetTheme() { resetTheme() {
this.colorTheme = DESCRIPTORS.colorTheme.fromStore(undefined); this.colorTheme = this.dependencies.preferenceDescriptors.colorTheme.fromStore(undefined);
} }
@action @action
protected fromStore({ preferences }: Partial<UserStoreModel> = {}) { protected fromStore({ preferences }: Partial<UserStoreModel> = {}) {
this.dependencies.logger.debug("UserStore.fromStore()", { preferences }); this.dependencies.logger.debug("UserStore.fromStore()", { preferences });
for (const [key, { fromStore }] of object.entries(DESCRIPTORS)) { for (const [key, { fromStore }] of object.entries(this.dependencies.preferenceDescriptors)) {
const curVal = this[key]; const curVal = this[key];
const newVal = fromStore((preferences)?.[key] as never) as never; const newVal = fromStore((preferences)?.[key] as never) as never;
@ -140,7 +141,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
toJSON(): UserStoreModel { toJSON(): UserStoreModel {
const preferences = object.fromEntries( const preferences = object.fromEntries(
object.entries(DESCRIPTORS) object.entries(this.dependencies.preferenceDescriptors)
.map(([key, { toStore }]) => [key, toStore(this[key] as never)]), .map(([key, { toStore }]) => [key, toStore(this[key] as never)]),
) as UserPreferencesModel; ) as UserPreferencesModel;

View File

@ -2524,12 +2524,40 @@ exports[`remove helm repository from list of active repositories in preferences
Synced Items Synced Items
</div> </div>
<div <div>
class="loading-spinner"
>
<div <div
class="Spinner singleColor" class="item flex gaps align-center justify-space-between mt-3"
/> >
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="folder"
>
folder
</span>
</i>
<div
class="flex-grow break-all"
>
/some-home-dir/.kube
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="delete"
>
delete
</span>
</i>
<div>
Remove
</div>
</div>
</div> </div>
</section> </section>
</div> </div>

View File

@ -8,12 +8,13 @@ import { withInjectables } from "@ogre-tools/injectable-react";
import type { UserStore } from "../../../../../../common/user-store"; import type { UserStore } from "../../../../../../common/user-store";
import userStoreInjectable from "../../../../../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../../../../../common/user-store/user-store.injectable";
import { Select } from "../../../../../../renderer/components/select"; import { Select } from "../../../../../../renderer/components/select";
import { defaultLocaleTimezone } from "../../../../../../common/user-store/preferences-helpers";
import moment from "moment-timezone"; import moment from "moment-timezone";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import currentTimezoneInjectable from "../../../../../../common/user-store/current-timezone.injectable";
interface Dependencies { interface Dependencies {
userStore: UserStore; userStore: UserStore;
currentTimezone: string;
} }
const timezoneOptions = moment.tz.names() const timezoneOptions = moment.tz.names()
@ -23,26 +24,26 @@ const timezoneOptions = moment.tz.names()
})); }));
const NonInjectedTimezone = observer(({ userStore }: Dependencies) => ( const NonInjectedTimezone = observer(({
userStore,
currentTimezone,
}: Dependencies) => (
<section id="locale"> <section id="locale">
<SubTitle title="Locale Timezone" /> <SubTitle title="Locale Timezone" />
<Select <Select
id="timezone-input" id="timezone-input"
options={timezoneOptions} options={timezoneOptions}
value={userStore.localeTimezone} value={userStore.localeTimezone}
onChange={value => userStore.localeTimezone = value?.value ?? defaultLocaleTimezone} onChange={value => userStore.localeTimezone = value?.value ?? currentTimezone}
themeName="lens" themeName="lens"
/> />
</section> </section>
)); ));
export const Timezone = withInjectables<Dependencies>( export const Timezone = withInjectables<Dependencies>(NonInjectedTimezone, {
NonInjectedTimezone, getProps: (di) => ({
userStore: di.inject(userStoreInjectable),
{ currentTimezone: di.inject(currentTimezoneInjectable),
getProps: (di) => ({ }),
userStore: di.inject(userStoreInjectable), });
}),
},
);

View File

@ -0,0 +1,20 @@
/**
* 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 type { SyncKind } from "./discover-sync-kind.injectable";
import discoverKubeconfigSyncKindInjectable from "./discover-sync-kind.injectable";
export type DiscoverAllKubeconfigSyncKinds = (filePaths: string[]) => Promise<[string, SyncKind][]>;
const discoverAllKubeconfigSyncKindsInjectable = getInjectable({
id: "discover-all-kubeconfig-sync-kinds",
instantiate: (di): DiscoverAllKubeconfigSyncKinds => {
const discoverKubeconfigSyncKind = di.inject(discoverKubeconfigSyncKindInjectable);
return filePaths => Promise.all(filePaths.map(discoverKubeconfigSyncKind));
},
});
export default discoverAllKubeconfigSyncKindsInjectable;

View File

@ -0,0 +1,46 @@
/**
* 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 statInjectable from "../../../../../../common/fs/stat.injectable";
import loggerInjectable from "../../../../../../common/logger.injectable";
export interface SyncKind {
type: "file" | "folder" | "unknown";
}
export type DiscoverKubeconfigSyncKind = (path: string) => Promise<[string, SyncKind]>;
const discoverKubeconfigSyncKindInjectable = getInjectable({
id: "discover-kubeconfig-sync-kind",
instantiate: (di): DiscoverKubeconfigSyncKind => {
const stat = di.inject(statInjectable);
const logger = di.inject(loggerInjectable);
return async (path) => {
try {
// stat follows the stat(2) linux syscall spec, namely it follows symlinks
const stats = await stat(path);
if (stats.isFile()) {
return [path, { type: "file" }];
}
if (stats.isDirectory()) {
return [path, { type: "folder" }];
}
logger.warn("[KUBECONFIG-SYNCS]: unknown stat entry", { stats });
return [path, { type: "unknown" }];
} catch (error) {
logger.warn(`[KUBECONFIG-SYNCS]: failed to stat entry: ${error}`, { error });
return [path, { type: "unknown" }];
}
};
},
});
export default discoverKubeconfigSyncKindInjectable;

View File

@ -3,12 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import fse from "fs-extra"; import { action, computed, makeObservable, observable, reaction } from "mobx";
import { computed, makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import React from "react"; import React from "react";
import { Notice } from "../../../../../../renderer/components/+extensions/notice"; import { Notice } from "../../../../../../renderer/components/+extensions/notice";
import type { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../../../../common/user-store"; import type { UserStore } from "../../../../../../common/user-store";
import { iter, tuple } from "../../../../../../renderer/utils"; import { iter, tuple } from "../../../../../../renderer/utils";
import { SubTitle } from "../../../../../../renderer/components/layout/sub-title"; import { SubTitle } from "../../../../../../renderer/components/layout/sub-title";
import { PathPicker } from "../../../../../../renderer/components/path-picker/path-picker"; import { PathPicker } from "../../../../../../renderer/components/path-picker/path-picker";
@ -18,56 +17,26 @@ import userStoreInjectable from "../../../../../../common/user-store/user-store.
import isWindowsInjectable from "../../../../../../common/vars/is-windows.injectable"; import isWindowsInjectable from "../../../../../../common/vars/is-windows.injectable";
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 type { DiscoverAllKubeconfigSyncKinds } from "./discover-all-sync-kinds.injectable";
import type { DiscoverKubeconfigSyncKind, SyncKind } from "./discover-sync-kind.injectable";
import discoverKubeconfigSyncKindInjectable from "./discover-sync-kind.injectable";
import discoverAllKubeconfigSyncKindsInjectable from "./discover-all-sync-kinds.injectable";
interface SyncInfo { interface Entry extends SyncKind {
type: "file" | "folder" | "unknown";
}
interface Entry extends Value {
filePath: string; filePath: string;
} }
interface Value {
data: KubeconfigSyncValue;
info: SyncInfo;
}
async function getMapEntry({ filePath, ...data }: KubeconfigSyncEntry, logger: Logger): Promise<[string, Value]> {
try {
// stat follows the stat(2) linux syscall spec, namely it follows symlinks
const stats = await fse.stat(filePath);
if (stats.isFile()) {
return [filePath, { info: { type: "file" }, data }];
}
if (stats.isDirectory()) {
return [filePath, { info: { type: "folder" }, data }];
}
logger.warn("[KubeconfigSyncs]: unknown stat entry", { stats });
return [filePath, { info: { type: "unknown" }, data }];
} catch (error) {
logger.warn(`[KubeconfigSyncs]: failed to stat entry: ${error}`, { error });
return [filePath, { info: { type: "unknown" }, data }];
}
}
export async function getAllEntries(filePaths: string[], logger: Logger): Promise<[string, Value][]> {
return Promise.all(filePaths.map(filePath => getMapEntry({ filePath }, logger)));
}
interface Dependencies { interface Dependencies {
userStore: UserStore; userStore: UserStore;
isWindows: boolean; isWindows: boolean;
logger: Logger; logger: Logger;
discoverAllKubeconfigSyncKinds: DiscoverAllKubeconfigSyncKinds;
discoverKubeconfigSyncKind: DiscoverKubeconfigSyncKind;
} }
@observer @observer
class NonInjectedKubeconfigSync extends React.Component<Dependencies> { class NonInjectedKubeconfigSync extends React.Component<Dependencies> {
syncs = observable.map<string, Value>(); readonly syncs = observable.map<string, SyncKind>();
@observable loaded = false; @observable loaded = false;
constructor(props: Dependencies) { constructor(props: Dependencies) {
@ -79,7 +48,7 @@ class NonInjectedKubeconfigSync extends React.Component<Dependencies> {
const mapEntries = await Promise.all( const mapEntries = await Promise.all(
iter.map( iter.map(
this.props.userStore.syncKubeconfigEntries, this.props.userStore.syncKubeconfigEntries,
([filePath, ...value]) => getMapEntry({ filePath, ...value }, this.props.logger), ([filePath]) => this.props.discoverKubeconfigSyncKind(filePath),
), ),
); );
@ -88,9 +57,13 @@ class NonInjectedKubeconfigSync extends React.Component<Dependencies> {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction( reaction(
() => Array.from(this.syncs.entries(), ([filePath, { data }]) => tuple.from(filePath, data)), () => Array.from(this.syncs.entries(), ([filePath, kind]) => tuple.from(filePath, kind)),
syncs => { syncs => {
this.props.userStore.syncKubeconfigEntries.replace(syncs); action(() => {
for (const [path] of syncs) {
this.props.userStore.syncKubeconfigEntries.set(path, {});
}
});
}, },
), ),
]); ]);
@ -105,11 +78,11 @@ class NonInjectedKubeconfigSync extends React.Component<Dependencies> {
} }
onPick = async (filePaths: string[]) => { onPick = async (filePaths: string[]) => {
this.syncs.merge(await getAllEntries(filePaths, this.props.logger)); this.syncs.merge(await this.props.discoverAllKubeconfigSyncKinds(filePaths));
}; };
getIconName(entry: Entry) { getIconName(entry: Entry) {
switch (entry.info.type) { switch (entry.type) {
case "file": case "file":
return "description"; return "description";
case "folder": case "folder":
@ -206,14 +179,12 @@ class NonInjectedKubeconfigSync extends React.Component<Dependencies> {
} }
} }
export const KubeconfigSync = withInjectables<Dependencies>( export const KubeconfigSync = withInjectables<Dependencies>(NonInjectedKubeconfigSync, {
NonInjectedKubeconfigSync, getProps: (di) => ({
userStore: di.inject(userStoreInjectable),
{ isWindows: di.inject(isWindowsInjectable),
getProps: (di) => ({ logger: di.inject(loggerInjectable),
userStore: di.inject(userStoreInjectable), discoverAllKubeconfigSyncKinds: di.inject(discoverAllKubeconfigSyncKindsInjectable),
isWindows: di.inject(isWindowsInjectable), discoverKubeconfigSyncKind: di.inject(discoverKubeconfigSyncKindInjectable),
logger: di.inject(loggerInjectable), }),
}), });
},
);

View File

@ -22,9 +22,9 @@ import kubeconfigSyncsInjectable from "../../../common/user-store/kubeconfig-syn
import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable"; import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import type { AsyncFnMock } from "@async-fn/jest"; import type { AsyncFnMock } from "@async-fn/jest";
import type { Stat } from "../../../common/fs/stat/stat.injectable"; import type { Stat } from "../../../common/fs/stat.injectable";
import asyncFn from "@async-fn/jest"; import asyncFn from "@async-fn/jest";
import statInjectable from "../../../common/fs/stat/stat.injectable"; import statInjectable from "../../../common/fs/stat.injectable";
import type { Watcher } from "../../../common/fs/watch/watch.injectable"; import type { Watcher } from "../../../common/fs/watch/watch.injectable";
import watchInjectable from "../../../common/fs/watch/watch.injectable"; import watchInjectable from "../../../common/fs/watch/watch.injectable";
import EventEmitter from "events"; import EventEmitter from "events";

View File

@ -10,7 +10,7 @@ import path from "path";
import { inspect } from "util"; import { inspect } from "util";
import type { CatalogEntity } from "../../../common/catalog"; import type { CatalogEntity } from "../../../common/catalog";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import statInjectable from "../../../common/fs/stat/stat.injectable"; import statInjectable from "../../../common/fs/stat.injectable";
import type { Watcher } from "../../../common/fs/watch/watch.injectable"; import type { Watcher } from "../../../common/fs/watch/watch.injectable";
import watchInjectable from "../../../common/fs/watch/watch.injectable"; import watchInjectable from "../../../common/fs/watch/watch.injectable";
import type { Disposer } from "../../../common/utils"; import type { Disposer } from "../../../common/utils";

View File

@ -9,7 +9,7 @@ import helmChartManagerCacheInjectable from "./helm-chart-manager-cache.injectab
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import execHelmInjectable from "./exec-helm/exec-helm.injectable"; import execHelmInjectable from "./exec-helm/exec-helm.injectable";
import readFileInjectable from "../../common/fs/read-file.injectable"; import readFileInjectable from "../../common/fs/read-file.injectable";
import statInjectable from "../../common/fs/stat/stat.injectable"; import statInjectable from "../../common/fs/stat.injectable";
const helmChartManagerInjectable = getInjectable({ const helmChartManagerInjectable = getInjectable({
id: "helm-chart-manager", id: "helm-chart-manager",

View File

@ -11,7 +11,7 @@ import type { Logger } from "../../common/logger";
import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable"; import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
import type { ExecHelm } from "./exec-helm/exec-helm.injectable"; import type { ExecHelm } from "./exec-helm/exec-helm.injectable";
import type { ReadFile } from "../../common/fs/read-file.injectable"; import type { ReadFile } from "../../common/fs/read-file.injectable";
import type { Stat } from "../../common/fs/stat/stat.injectable"; import type { Stat } from "../../common/fs/stat.injectable";
export interface HelmCacheFile { export interface HelmCacheFile {
apiVersion: string; apiVersion: string;

View File

@ -23,7 +23,7 @@ import userShellSettingInjectable from "../../../common/user-store/shell-setting
import appNameInjectable from "../../../common/vars/app-name.injectable"; import appNameInjectable from "../../../common/vars/app-name.injectable";
import buildVersionInjectable from "../../vars/build-version/build-version.injectable"; import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable";
import statInjectable from "../../../common/fs/stat/stat.injectable"; import statInjectable from "../../../common/fs/stat.injectable";
export interface OpenLocalShellSessionArgs { export interface OpenLocalShellSessionArgs {
websocket: WebSocket; websocket: WebSocket;

View File

@ -18,7 +18,7 @@ import userShellSettingInjectable from "../../../common/user-store/shell-setting
import appNameInjectable from "../../../common/vars/app-name.injectable"; import appNameInjectable from "../../../common/vars/app-name.injectable";
import buildVersionInjectable from "../../vars/build-version/build-version.injectable"; import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable";
import statInjectable from "../../../common/fs/stat/stat.injectable"; import statInjectable from "../../../common/fs/stat.injectable";
export interface NodeShellSessionArgs { export interface NodeShellSessionArgs {
websocket: WebSocket; websocket: WebSocket;

View File

@ -17,7 +17,7 @@ import type { ComputeShellEnvironment } from "../../features/shell-sync/main/com
import type { SpawnPty } from "./spawn-pty.injectable"; import type { SpawnPty } from "./spawn-pty.injectable";
import type { InitializableState } from "../../common/initializable-state/create"; import type { InitializableState } from "../../common/initializable-state/create";
import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable"; import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable";
import type { Stat } from "../../common/fs/stat/stat.injectable"; import type { Stat } from "../../common/fs/stat.injectable";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
export class ShellOpenError extends Error { export class ShellOpenError extends Error {

View File

@ -13,7 +13,7 @@ import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor"; import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor"; import { renderFor } from "../../test-utils/renderFor";
import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable"; import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable";
import statInjectable from "../../../../common/fs/stat/stat.injectable"; import statInjectable from "../../../../common/fs/stat.injectable";
describe("ClusterLocalTerminalSettings", () => { describe("ClusterLocalTerminalSettings", () => {
let render: DiRender; let render: DiRender;

View File

@ -69,6 +69,9 @@ import { getCompositePaths } from "../../../common/utils/composite/get-composite
import { discoverFor } from "./discovery-of-html-elements"; import { discoverFor } from "./discovery-of-html-elements";
import { findComposite } from "../../../common/utils/composite/find-composite/find-composite"; import { findComposite } from "../../../common/utils/composite/find-composite/find-composite";
import shouldStartHiddenInjectable from "../../../main/electron-app/features/should-start-hidden.injectable"; import shouldStartHiddenInjectable from "../../../main/electron-app/features/should-start-hidden.injectable";
import fsInjectable from "../../../common/fs/fs.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import homeDirectoryPathInjectable from "../../../common/os/home-directory-path.injectable";
type Callback = (di: DiContainer) => void | Promise<void>; type Callback = (di: DiContainer) => void | Promise<void>;
@ -179,6 +182,15 @@ export const getApplicationBuilder = () => {
overrideFsWithFakes(mainDi); overrideFsWithFakes(mainDi);
// Set up ~/.kube as existing as a folder
{
const { ensureDirSync } = mainDi.inject(fsInjectable);
const joinPaths = mainDi.inject(joinPathsInjectable);
const homeDirectoryPath = mainDi.inject(homeDirectoryPathInjectable);
ensureDirSync(joinPaths(homeDirectoryPath, ".kube"));
}
let environment = environments.application; let environment = environments.application;
mainDi.override(mainExtensionsInjectable, (di) => { mainDi.override(mainExtensionsInjectable, (di) => {

View File

@ -2,13 +2,13 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getAllEntries } from "../../features/preferences/renderer/preference-items/kubernetes/kubeconfig-sync/kubeconfig-sync";
import { Notifications } from "../components/notifications"; import { Notifications } from "../components/notifications";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import React from "react"; import React from "react";
import navigateToKubernetesPreferencesInjectable from "../../features/preferences/common/navigate-to-kubernetes-preferences.injectable"; import navigateToKubernetesPreferencesInjectable from "../../features/preferences/common/navigate-to-kubernetes-preferences.injectable";
import loggerInjectable from "../../common/logger.injectable"; import discoverAllKubeconfigSyncKindsInjectable from "../../features/preferences/renderer/preference-items/kubernetes/kubeconfig-sync/discover-all-sync-kinds.injectable";
import { action } from "mobx";
const addSyncEntriesInjectable = getInjectable({ const addSyncEntriesInjectable = getInjectable({
id: "add-sync-entries", id: "add-sync-entries",
@ -16,12 +16,16 @@ const addSyncEntriesInjectable = getInjectable({
instantiate: (di) => { instantiate: (di) => {
const userStore = di.inject(userStoreInjectable); const userStore = di.inject(userStoreInjectable);
const navigateToKubernetesPreferences = di.inject(navigateToKubernetesPreferencesInjectable); const navigateToKubernetesPreferences = di.inject(navigateToKubernetesPreferencesInjectable);
const logger = di.inject(loggerInjectable); const discoverAllKubeconfigSyncKinds = di.inject(discoverAllKubeconfigSyncKindsInjectable);
return async (filePaths: string[]) => { return async (filePaths: string[]) => {
userStore.syncKubeconfigEntries.merge( const kinds = await discoverAllKubeconfigSyncKinds(filePaths);
await getAllEntries(filePaths, logger),
); action(() => {
for (const [path] of kinds) {
userStore.syncKubeconfigEntries.set(path, {});
}
});
Notifications.ok(( Notifications.ok((
<div> <div>