Technical requirements for behavioural unit tests (#5084)

* Implement a lot of technical requirements for behavioural unit tests

Note: the crux of this was to make routing env-agnostic, and not based on URLs as magic strings, but instead something type-enforced.

Note: extension-based routes comply to same exact interface by "late-registering" their routes when installed. Routes are just injectables.

Note: another chunk of global shared state is no more.

Note: a lot of explicit side effects have been cornered to injectables.

Note: a lot of stuff has become reactive as part if this.

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make a directory commonly available

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Require id for <Select /> to prevent non-deterministic renders

This was caused by global state in a 3rd party lib: "react-select".

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Specify id for all <Select /> to satisfy previous commit

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Prevent explicit side effect in component by using existing dependency instead

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract instantiation of "conf" as injectables for causing side effects

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce a legacy-helper to make gradual refactoring of inheritors of Singleton easier

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make legacy unit tests for hotbar green and more simple by using the new legacy helper

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Temporarily kludge all unit tests green with a disclaimer about allowing side-effects

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove kludge in previous commit by explicitly permitting specific side effects where old unit tests require it

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Prevent old unit test with side effects from accessing file system

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Migrate to actual typing for di.permitSideEffects

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Prevent unit tests from failing because of non-standard method of HTML-element not present in js-dom

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Adapt integration tests to recent changes

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Fix code style

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Fix artifact from bad rebase

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Add a deprecation from a review comment

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove change that is not required

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant comment

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix code style

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant file

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix bad merge

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Improve variable name

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Tweak logger interface to be more descriptive

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make injecting legacy singleton always provide new instance

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove conditional typing when not needed

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Improve naming of variable

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove unnecessary code style changes

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove flag for causing side effects from too broad scope

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Override side-effects in unit test using injectable instead of monkey patching

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Flag some side-effects and add general overrides

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Fix unit tests in CI by removing explicit side-effect

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove explicit side-effect from getting default shell

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce abstraction for getting absolute paths

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using abstraction for getting absolute path to control explicit side effect

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce abstraction for joining paths

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using abstraction for joining paths to control explicit side effect

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix fake implementation for join paths

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix test after removing explicit side effect

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove explicit side effects from kubeconfig-syncs

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix arguments after removing explicit side effect

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make registrators not async for not being needed anymore

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make generalCatalogEntities non-observable, as there is no requirement

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove redundant code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify logic for registering general catalog entity sources

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add TODO

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Replace function for getting application menu items with reactive solution

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix typo in interface name

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove global shared state usages of hot bar store

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant enum

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Iku-tursoandJanne Savolainen authored and GitHub committed 2022-03-31 16:57:05 +03:00
1 parent ab2cd27a27
commit a277cfcf02
583 files changed
+13073 -6556

No files matched your search

@@ -0,0 +1,21 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import appPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/app/app-preferences-route.injectable";
import { Application } from "./application";
const appPreferencesRouteComponentInjectable = getInjectable({
id: "app-preferences-route-component",
instantiate: (di) => ({
route: di.inject(appPreferencesRouteInjectable),
Component: Application,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default appPreferencesRouteComponentInjectable;
@@ -7,8 +7,8 @@ import React from "react";
import { observer } from "mobx-react";
import { SubTitle } from "../layout/sub-title";
import { Select, SelectOption } from "../select";
import { ThemeStore } from "../../theme.store";
import { UserStore } from "../../../common/user-store";
import type { ThemeStore } from "../../theme.store";
import type { UserStore } from "../../../common/user-store";
import { Input } from "../input";
import { Switch } from "../switch";
import moment from "moment-timezone";
@@ -19,6 +19,9 @@ import { ExtensionSettings } from "./extension-settings";
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
import { withInjectables } from "@ogre-tools/injectable-react";
import appPreferencesInjectable from "./app-preferences/app-preferences.injectable";
import { Preferences } from "./preferences";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import themeStoreInjectable from "../../theme-store.injectable";
const timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({
label: zone,
@@ -31,100 +34,106 @@ const updateChannelOptions: SelectOption<string>[] = Array.from(
interface Dependencies {
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>;
userStore: UserStore;
themeStore: ThemeStore;
}
const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems }) => {
const userStore = UserStore.getInstance();
const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, userStore, themeStore }) => {
const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
const extensionSettings = appPreferenceItems.get().filter((preference) => preference.showInPreferencesTab === "application");
const themeStore = ThemeStore.getInstance();
return (
<section id="application">
<h2 data-testid="application-header">Application</h2>
<section id="appearance">
<SubTitle title="Theme" />
<Select
options={[
{ label: "Sync with computer", value: "system" },
...themeStore.themeOptions,
]}
value={userStore.colorTheme}
onChange={({ value }) => userStore.colorTheme = value}
themeName="lens"
/>
<Preferences data-testid="application-preferences-page">
<section id="application">
<h2 data-testid="application-header">Application</h2>
<section id="appearance">
<SubTitle title="Theme" />
<Select
id="theme-input"
options={[
{ label: "Sync with computer", value: "system" },
...themeStore.themeOptions,
]}
value={userStore.colorTheme}
onChange={({ value }) => userStore.colorTheme = value}
themeName="lens"
/>
</section>
<hr />
<section id="extensionRegistryUrl">
<SubTitle title="Extension Install Registry" />
<Select
id="extension-install-registry-input"
options={Object.values(ExtensionRegistryLocation)}
value={userStore.extensionRegistryUrl.location}
onChange={action(({ value }) => {
userStore.extensionRegistryUrl.location = value;
if (userStore.extensionRegistryUrl.location === ExtensionRegistryLocation.CUSTOM) {
userStore.extensionRegistryUrl.customUrl = "";
}
})}
themeName="lens"
/>
<p className="mt-4 mb-5 leading-relaxed">
This setting is to change the registry URL for installing extensions by name.{" "}
If you are unable to access the default registry ({defaultExtensionRegistryUrl}){" "}
you can change it in your <b>.npmrc</b>&nbsp;file or in the input below.
</p>
<Input
theme="round-black"
validators={isUrl}
value={customUrl}
onChange={setCustomUrl}
onBlur={() => userStore.extensionRegistryUrl.customUrl = customUrl}
placeholder="Custom Extension Registry URL..."
disabled={userStore.extensionRegistryUrl.location !== ExtensionRegistryLocation.CUSTOM}
/>
</section>
<hr />
<section id="other">
<SubTitle title="Start-up" />
<Switch checked={userStore.openAtLogin} onChange={() => userStore.openAtLogin = !userStore.openAtLogin}>
Automatically start Lens on login
</Switch>
</section>
<hr />
{extensionSettings.map(setting => (
<ExtensionSettings key={setting.id} setting={setting} size="normal" />
))}
<section id="update-channel">
<SubTitle title="Update Channel" />
<Select
id="update-channel-input"
options={updateChannelOptions}
value={userStore.updateChannel}
onChange={({ value }) => userStore.updateChannel = value}
themeName="lens"
/>
</section>
<hr />
<section id="locale">
<SubTitle title="Locale Timezone" />
<Select
id="timezone-input"
options={timezoneOptions}
value={userStore.localeTimezone}
onChange={({ value }) => userStore.setLocaleTimezone(value)}
themeName="lens"
/>
</section>
</section>
<hr/>
<section id="extensionRegistryUrl">
<SubTitle title="Extension Install Registry" />
<Select
options={Object.values(ExtensionRegistryLocation)}
value={userStore.extensionRegistryUrl.location}
onChange={action(({ value }) => {
userStore.extensionRegistryUrl.location = value;
if (userStore.extensionRegistryUrl.location === ExtensionRegistryLocation.CUSTOM) {
userStore.extensionRegistryUrl.customUrl = "";
}
})}
themeName="lens"
/>
<p className="mt-4 mb-5 leading-relaxed">
This setting is to change the registry URL for installing extensions by name.{" "}
If you are unable to access the default registry ({defaultExtensionRegistryUrl}){" "}
you can change it in your <b>.npmrc</b>&nbsp;file or in the input below.
</p>
<Input
theme="round-black"
validators={isUrl}
value={customUrl}
onChange={setCustomUrl}
onBlur={() => userStore.extensionRegistryUrl.customUrl = customUrl}
placeholder="Custom Extension Registry URL..."
disabled={userStore.extensionRegistryUrl.location !== ExtensionRegistryLocation.CUSTOM}
/>
</section>
<hr />
<section id="other">
<SubTitle title="Start-up" />
<Switch checked={userStore.openAtLogin} onChange={() => userStore.openAtLogin = !userStore.openAtLogin}>
Automatically start Lens on login
</Switch>
</section>
<hr />
{extensionSettings.map(setting => (
<ExtensionSettings key={setting.id} setting={setting} size="normal" />
))}
<section id="update-channel">
<SubTitle title="Update Channel" />
<Select
options={updateChannelOptions}
value={userStore.updateChannel}
onChange={({ value }) => userStore.updateChannel = value}
themeName="lens"
/>
</section>
<hr />
<section id="locale">
<SubTitle title="Locale Timezone" />
<Select
options={timezoneOptions}
value={userStore.localeTimezone}
onChange={({ value }) => userStore.setLocaleTimezone(value)}
themeName="lens"
/>
</section>
</section>
</Preferences>
);
};
@@ -134,6 +143,8 @@ export const Application = withInjectables<Dependencies>(
{
getProps: (di) => ({
appPreferenceItems: di.inject(appPreferencesInjectable),
userStore: di.inject(userStoreInjectable),
themeStore: di.inject(themeStoreInjectable),
}),
},
);
@@ -0,0 +1,28 @@
/**
* 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 observableHistoryInjectable from "../../navigation/observable-history.injectable";
import navigateToFrontPageInjectable from "../../../common/front-end-routing/navigate-to-front-page.injectable";
const closePreferencesInjectable = getInjectable({
id: "close-preferences",
instantiate: (di) => {
const observableHistory = di.inject(observableHistoryInjectable);
const navigateToFrontPage = di.inject(navigateToFrontPageInjectable);
return () => {
if (observableHistory.length === 1) {
navigateToFrontPage();
return;
}
observableHistory.goBack();
};
},
});
export default closePreferencesInjectable;
@@ -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 isWindowsInjectable from "../../../common/vars/is-windows.injectable";
const defaultShellInjectable = getInjectable({
id: "default-shell",
instantiate: (di) => {
const isWindows = di.inject(isWindowsInjectable);
return (
process.env.SHELL ||
process.env.PTYSHELL ||
(isWindows ? "powershell.exe" : "System default shell")
);
},
causesSideEffects: true,
});
export default defaultShellInjectable;
@@ -0,0 +1,21 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import { Editor } from "./editor";
import editorPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/editor/editor-preferences-route.injectable";
const editorPreferencesRouteComponentInjectable = getInjectable({
id: "editor-preferences-route-component",
instantiate: (di) => ({
route: di.inject(editorPreferencesRouteInjectable),
Component: Editor,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default editorPreferencesRouteComponentInjectable;
+91 -66
View File
@@ -4,12 +4,15 @@
*/
import { observer } from "mobx-react";
import React from "react";
import { UserStore } from "../../../common/user-store";
import type { UserStore } from "../../../common/user-store";
import { Switch } from "../switch";
import { Select } from "../select";
import { SubTitle } from "../layout/sub-title";
import { SubHeader } from "../layout/sub-header";
import { Input, InputValidators } from "../input";
import { Preferences } from "./preferences";
import { withInjectables } from "@ogre-tools/injectable-react";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
enum EditorLineNumbersStyles {
on = "On",
@@ -18,78 +21,100 @@ enum EditorLineNumbersStyles {
interval = "Interval",
}
export const Editor = observer(() => {
const editorConfiguration = UserStore.getInstance().editorConfiguration;
interface Dependencies {
userStore: UserStore;
}
const NonInjectedEditor = observer(({ userStore }: Dependencies) => {
const editorConfiguration = userStore.editorConfiguration;
return (
<section id="editor">
<h2 data-testid="editor-configuration-header">Editor configuration</h2>
<Preferences data-testid="editor-preferences-page">
<section id="editor">
<h2 data-testid="editor-configuration-header">Editor configuration</h2>
<SubTitle title="Minimap"/>
<section>
<div className="flex gaps justify-space-between">
<div className="flex gaps align-center">
<Switch
checked={editorConfiguration.minimap.enabled}
onChange={() => editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled}
>
Show minimap
</Switch>
<SubTitle title="Minimap" />
<section>
<div className="flex gaps justify-space-between">
<div className="flex gaps align-center">
<Switch
checked={editorConfiguration.minimap.enabled}
onChange={() => editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled}
>
Show minimap
</Switch>
</div>
<div className="flex gaps align-center">
<SubHeader compact>Position</SubHeader>
<Select
id="minimap-input"
themeName="lens"
options={["left", "right"]}
value={editorConfiguration.minimap.side}
onChange={({ value }) => editorConfiguration.minimap.side = value}
/>
</div>
</div>
<div className="flex gaps align-center">
<SubHeader compact>Position</SubHeader>
<Select
themeName="lens"
options={["left", "right"]}
value={editorConfiguration.minimap.side}
onChange={({ value }) => editorConfiguration.minimap.side = value}
/>
</div>
</div>
</section>
</section>
<section>
<SubTitle title="Line numbers"/>
<Select
options={Object.entries(EditorLineNumbersStyles).map(([value, label]) => ({ label, value }))}
value={editorConfiguration.lineNumbers}
onChange={({ value }) => editorConfiguration.lineNumbers = value}
themeName="lens"
/>
</section>
<section>
<SubTitle title="Line numbers" />
<Select
id="editor-line-numbers-input"
options={Object.entries(EditorLineNumbersStyles).map(([value, label]) => ({
label,
value,
}))}
value={editorConfiguration.lineNumbers}
onChange={({ value }) => editorConfiguration.lineNumbers = value}
themeName="lens"
/>
</section>
<section>
<SubTitle title="Tab size"/>
<Input
theme="round-black"
type="number"
min={1}
validators={InputValidators.isNumber}
value={editorConfiguration.tabSize.toString()}
onChange={value => editorConfiguration.tabSize = Number(value)}
/>
<section>
<SubTitle title="Tab size" />
<Input
theme="round-black"
type="number"
min={1}
validators={InputValidators.isNumber}
value={editorConfiguration.tabSize.toString()}
onChange={value => editorConfiguration.tabSize = Number(value)}
/>
</section>
<section>
<SubTitle title="Font size" />
<Input
theme="round-black"
type="number"
min={10}
validators={InputValidators.isNumber}
value={editorConfiguration.fontSize.toString()}
onChange={value => editorConfiguration.fontSize = Number(value)}
/>
</section>
<section>
<SubTitle title="Font family" />
<Input
theme="round-black"
type="text"
value={editorConfiguration.fontFamily}
onChange={value => editorConfiguration.fontFamily = value}
/>
</section>
</section>
<section>
<SubTitle title="Font size"/>
<Input
theme="round-black"
type="number"
min={10}
validators={InputValidators.isNumber}
value={editorConfiguration.fontSize.toString()}
onChange={value => editorConfiguration.fontSize = Number(value)}
/>
</section>
<section>
<SubTitle title="Font family"/>
<Input
theme="round-black"
type="text"
value={editorConfiguration.fontFamily}
onChange={value => editorConfiguration.fontFamily = value}
/>
</section>
</section>
</Preferences>
);
});
export const Editor = withInjectables<Dependencies>(
NonInjectedEditor,
{
getProps: (di) => ({
userStore: di.inject(userStoreInjectable),
}),
},
);
@@ -0,0 +1,54 @@
/**
* 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 { filter, map } from "lodash/fp";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import { pipeline } from "@ogre-tools/fp";
import { extensionPreferenceItemInjectionToken } from "./extension-preference-items.injectable";
const extensionPreferenceItemRegistratorInjectable = getInjectable({
id: "extension-preference-item-registrator",
instantiate:
(di) =>
(extension: LensRendererExtension, extensionInstallationCount) => {
const injectables = pipeline(
extension.appPreferences,
filter(
(registration) => !registration.showInPreferencesTab,
),
map((registration) => {
const id = `extension-preferences-item-${registration.id}-for-extension-${extension.sanitizedExtensionId}`;
return getInjectable({
id: `${id}-for-instance-${extensionInstallationCount}`,
injectionToken: extensionPreferenceItemInjectionToken,
instantiate: () => ({
id: registration.id || id,
title: registration.title,
extension,
components: {
Hint: registration.components.Hint,
Input: registration.components.Input,
},
}),
});
}),
);
injectables.forEach(di.register);
return;
},
injectionToken: extensionRegistratorInjectionToken,
});
export default extensionPreferenceItemRegistratorInjectable;
@@ -0,0 +1,50 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import { filter, overSome } from "lodash/fp";
import { computed } from "mobx";
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
interface ExtensionPreferenceItem extends RegisteredAppPreference {
extension: LensRendererExtension;
}
export const extensionPreferenceItemInjectionToken = getInjectionToken<ExtensionPreferenceItem>({
id: "extension-preference-item-injection-token",
});
const extensionsPreferenceItemsInjectable = getInjectable({
id: "extension-preference-items",
instantiate: (di) => {
const extensions = di.inject(rendererExtensionsInjectable);
return computed(() => {
const enabledExtensions = extensions.get();
return pipeline(
di.injectMany(extensionPreferenceItemInjectionToken),
filter((item) =>
overSome([
isNonExtensionItem,
isEnabledExtensionItemFor(enabledExtensions),
])(item),
),
);
});
},
});
const isNonExtensionItem = (item: ExtensionPreferenceItem) => !item.extension;
const isEnabledExtensionItemFor =
(enabledExtensions: LensRendererExtension[]) => (item: ExtensionPreferenceItem) =>
!!enabledExtensions.find((extension) => extension === item.extension);
export default extensionsPreferenceItemsInjectable;
@@ -0,0 +1,21 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import extensionPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable";
import { Extensions } from "./extensions";
const extensionPreferencesRouteComponentInjectable = getInjectable({
id: "extension-preferences-route-component",
instantiate: (di) => ({
route: di.inject(extensionPreferencesRouteInjectable),
Component: Extensions,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default extensionPreferencesRouteComponentInjectable;
@@ -4,15 +4,15 @@
*/
import { SubTitle } from "../layout/sub-title";
import type { AppPreferenceRegistration } from "./app-preferences/app-preference-registration";
import React from "react";
import React, { DOMAttributes } from "react";
import { cssNames } from "../../../renderer/utils";
export interface ExtensionSettingsProps {
export interface ExtensionSettingsProps extends DOMAttributes<HTMLDivElement> {
setting: AppPreferenceRegistration;
size: "small" | "normal";
}
export function ExtensionSettings({ setting, size }: ExtensionSettingsProps) {
export function ExtensionSettings({ setting, size, ...props }: ExtensionSettingsProps) {
const {
title,
id,
@@ -21,7 +21,7 @@ export function ExtensionSettings({ setting, size }: ExtensionSettingsProps) {
return (
<React.Fragment>
<section id={id} className={cssNames(size)}>
<section id={id} className={cssNames(size)} {...props}>
<SubTitle title={title} />
<Input />
<div className="hint">
@@ -0,0 +1,54 @@
/**
* 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 { filter, map } from "lodash/fp";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import { pipeline } from "@ogre-tools/fp";
import { telemetryPreferenceItemInjectionToken } from "./telemetry-preference-items.injectable";
const extensionTelemetryPreferenceItemRegistratorInjectable = getInjectable({
id: "extension-telemetry-preference-item-registrator",
instantiate:
(di) =>
(extension: LensRendererExtension, extensionInstallationCount) => {
const injectables = pipeline(
extension.appPreferences,
filter(
(registration) => registration.showInPreferencesTab === "telemetry",
),
map((registration) => {
const id = `telemetry-preferences-item-${registration.id}-for-extension-${extension.sanitizedExtensionId}`;
return getInjectable({
id: `${id}-for-instance-${extensionInstallationCount}`,
injectionToken: telemetryPreferenceItemInjectionToken,
instantiate: () => ({
id: registration.id || id,
title: registration.title,
extension,
components: {
Hint: registration.components.Hint,
Input: registration.components.Input,
},
}),
});
}),
);
injectables.forEach(di.register);
return;
},
injectionToken: extensionRegistratorInjectionToken,
});
export default extensionTelemetryPreferenceItemRegistratorInjectable;
@@ -8,33 +8,36 @@ import type { IComputedValue } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
import appPreferencesInjectable from "./app-preferences/app-preferences.injectable";
import { ExtensionSettings } from "./extension-settings";
import { Preferences } from "./preferences";
import extensionsPreferenceItemsInjectable from "./extension-preference-items.injectable";
interface Dependencies {
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>;
preferenceItems: IComputedValue<RegisteredAppPreference[]>;
}
const NonInjectedExtensions: React.FC<Dependencies> = ({ appPreferenceItems }) => {
const settings = appPreferenceItems.get();
return (
const NonInjectedExtensions = ({ preferenceItems }: Dependencies) => (
<Preferences data-testid="extension-preferences-page">
<section id="extensions">
<h2>Extensions</h2>
{settings.filter(e => !e.showInPreferencesTab).map((setting) =>
<ExtensionSettings key={setting.id} setting={setting} size="small" />,
)}
{preferenceItems.get().map((preferenceItem) => (
<ExtensionSettings
key={preferenceItem.id}
setting={preferenceItem}
size="small"
data-testid={`extension-preference-item-for-${preferenceItem.id}`}
/>
))}
</section>
);
};
</Preferences>
);
export const Extensions = withInjectables<Dependencies>(
observer(NonInjectedExtensions),
{
getProps: (di) => ({
appPreferenceItems: di.inject(appPreferencesInjectable),
preferenceItems: di.inject(extensionsPreferenceItemsInjectable),
}),
},
);
@@ -144,7 +144,8 @@ export class HelmCharts extends React.Component {
return (
<div>
<div className="flex gaps">
<Select id="HelmRepoSelect"
<Select
id="HelmRepoSelect"
placeholder="Repositories"
isLoading={this.loadingAvailableRepos}
isDisabled={this.loadingAvailableRepos}
@@ -2,20 +2,22 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { withInjectables } from "@ogre-tools/injectable-react";
import fse from "fs-extra";
import { computed, makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import { Notice } from "../+extensions/notice";
import { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../common/user-store";
import { isWindows } from "../../../common/vars";
import logger from "../../../main/logger";
import type { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../common/user-store";
import { iter, tuple } from "../../utils";
import { SubTitle } from "../layout/sub-title";
import { PathPicker } from "../path-picker/path-picker";
import { Spinner } from "../spinner";
import { RemovableItem } from "./removable-item";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import type { Logger } from "../../../common/logger";
interface SyncInfo {
type: "file" | "folder" | "unknown";
@@ -30,7 +32,7 @@ interface Value {
info: SyncInfo;
}
async function getMapEntry({ filePath, ...data }: KubeconfigSyncEntry): Promise<[string, Value]> {
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);
@@ -53,16 +55,22 @@ async function getMapEntry({ filePath, ...data }: KubeconfigSyncEntry): Promise<
}
}
export async function getAllEntries(filePaths: string[]): Promise<[string, Value][]> {
return Promise.all(filePaths.map(filePath => getMapEntry({ filePath })));
export async function getAllEntries(filePaths: string[], logger: Logger): Promise<[string, Value][]> {
return Promise.all(filePaths.map(filePath => getMapEntry({ filePath }, logger)));
}
interface Dependencies {
userStore: UserStore;
isWindows: boolean;
logger: Logger;
}
@observer
export class KubeconfigSyncs extends React.Component {
class NonInjectedKubeconfigSyncs extends React.Component<Dependencies> {
syncs = observable.map<string, Value>();
@observable loaded = false;
constructor(props: {}) {
constructor(props: Dependencies) {
super(props);
makeObservable(this);
}
@@ -70,8 +78,8 @@ export class KubeconfigSyncs extends React.Component {
async componentDidMount() {
const mapEntries = await Promise.all(
iter.map(
UserStore.getInstance().syncKubeconfigEntries,
([filePath, ...value]) => getMapEntry({ filePath, ...value }),
this.props.userStore.syncKubeconfigEntries,
([filePath, ...value]) => getMapEntry({ filePath, ...value }, this.props.logger),
),
);
@@ -82,7 +90,7 @@ export class KubeconfigSyncs extends React.Component {
reaction(
() => Array.from(this.syncs.entries(), ([filePath, { data }]) => tuple.from(filePath, data)),
syncs => {
UserStore.getInstance().syncKubeconfigEntries.replace(syncs);
this.props.userStore.syncKubeconfigEntries.replace(syncs);
},
),
]);
@@ -97,7 +105,7 @@ export class KubeconfigSyncs extends React.Component {
}
onPick = async (filePaths: string[]) => {
this.syncs.merge(await getAllEntries(filePaths));
this.syncs.merge(await getAllEntries(filePaths, this.props.logger));
};
getIconName(entry: Entry) {
@@ -153,7 +161,7 @@ export class KubeconfigSyncs extends React.Component {
}
renderSyncButtons() {
if (isWindows) {
if (this.props.isWindows) {
return (
<div className="flex gaps align-center mb-5">
<PathPicker
@@ -195,3 +203,15 @@ export class KubeconfigSyncs extends React.Component {
);
}
}
export const KubeconfigSyncs = withInjectables<Dependencies>(
NonInjectedKubeconfigSyncs,
{
getProps: (di) => ({
userStore: di.inject(userStoreInjectable),
isWindows: di.inject(isWindowsInjectable),
logger: di.inject(loggerInjectable),
}),
},
);
@@ -7,92 +7,108 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
import { Input, InputValidators } from "../input";
import { SubTitle } from "../layout/sub-title";
import { UserStore } from "../../../common/user-store";
import type { UserStore } from "../../../common/user-store";
import { SelectOption, Select } from "../select";
import { Switch } from "../switch";
import { packageMirrors } from "../../../common/user-store/preferences-helpers";
import directoryForBinariesInjectable from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import { kubectlBinaryPath } from "../../../common/vars";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import directoryForKubectlBinariesInjectable from "../../../common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable";
interface Dependencies {
defaultPathForGeneralBinaries: string;
defaultPathForKubectlBinaries: string;
userStore: UserStore;
}
const NonInjectedKubectlBinaries: React.FC<Dependencies> = observer(({ defaultPathForKubectlBinaries }) => {
const userStore = UserStore.getInstance();
const [downloadPath, setDownloadPath] = useState(userStore.downloadBinariesPath || "");
const [binariesPath, setBinariesPath] = useState(userStore.kubectlBinariesPath || "");
const pathValidator = downloadPath ? InputValidators.isPath : undefined;
const downloadMirrorOptions: SelectOption<string>[] = Array.from(
packageMirrors.entries(),
([value, { label, platforms }]) => ({ value, label, platforms }),
);
const NonInjectedKubectlBinaries: React.FC<Dependencies> = observer(
({ defaultPathForGeneralBinaries, defaultPathForKubectlBinaries, userStore }) => {
const [downloadPath, setDownloadPath] = useState(
userStore.downloadBinariesPath || "",
);
const save = () => {
userStore.downloadBinariesPath = downloadPath;
userStore.kubectlBinariesPath = binariesPath;
};
const [binariesPath, setBinariesPath] = useState(
userStore.kubectlBinariesPath || "",
);
return (
<>
<section>
<SubTitle title="Kubectl binary download"/>
<Switch
checked={userStore.downloadKubectlBinaries}
onChange={() => userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries}
>
Download kubectl binaries matching the Kubernetes cluster version
</Switch>
</section>
const pathValidator = downloadPath ? InputValidators.isPath : undefined;
const downloadMirrorOptions: SelectOption<string>[] = Array.from(
packageMirrors.entries(),
([value, { label, platforms }]) => ({ value, label, platforms }),
);
<section>
<SubTitle title="Download mirror" />
<Select
placeholder="Download mirror for kubectl"
options={downloadMirrorOptions}
value={userStore.downloadMirror}
onChange={({ value }: SelectOption) => userStore.downloadMirror = value}
disabled={!userStore.downloadKubectlBinaries}
isOptionDisabled={({ platforms }) => !platforms.has(process.platform)}
themeName="lens"
/>
</section>
const save = () => {
userStore.downloadBinariesPath = downloadPath;
userStore.kubectlBinariesPath = binariesPath;
};
<section>
<SubTitle title="Directory for binaries" />
<Input
theme="round-black"
value={downloadPath}
placeholder={defaultPathForKubectlBinaries}
validators={pathValidator}
onChange={setDownloadPath}
onBlur={save}
disabled={!userStore.downloadKubectlBinaries}
/>
<div className="hint">
The directory to download binaries into.
</div>
</section>
return (
<>
<section>
<SubTitle title="Kubectl binary download" />
<Switch
checked={userStore.downloadKubectlBinaries}
onChange={() => userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries}
>
Download kubectl binaries matching the Kubernetes cluster version
</Switch>
</section>
<section>
<SubTitle title="Path to kubectl binary" />
<Input
theme="round-black"
placeholder={kubectlBinaryPath.get()}
value={binariesPath}
validators={pathValidator}
onChange={setBinariesPath}
onBlur={save}
disabled={userStore.downloadKubectlBinaries}
/>
</section>
</>
);
});
<section>
<SubTitle title="Download mirror" />
<Select
id="download-mirror-input"
placeholder="Download mirror for kubectl"
options={downloadMirrorOptions}
value={userStore.downloadMirror}
onChange={({ value }: SelectOption) => userStore.downloadMirror = value}
disabled={!userStore.downloadKubectlBinaries}
isOptionDisabled={({ platforms }) =>
!platforms.has(process.platform)
}
themeName="lens"
/>
</section>
export const KubectlBinaries = withInjectables<Dependencies>(NonInjectedKubectlBinaries, {
getProps: (di) => ({
defaultPathForKubectlBinaries: di.inject(directoryForBinariesInjectable),
}),
});
<section>
<SubTitle title="Directory for binaries" />
<Input
theme="round-black"
value={downloadPath}
placeholder={defaultPathForGeneralBinaries}
validators={pathValidator}
onChange={setDownloadPath}
onBlur={save}
disabled={!userStore.downloadKubectlBinaries}
/>
<div className="hint">The directory to download binaries into.</div>
</section>
<section>
<SubTitle title="Path to kubectl binary" />
<Input
theme="round-black"
placeholder={defaultPathForKubectlBinaries}
value={binariesPath}
validators={pathValidator}
onChange={setBinariesPath}
onBlur={save}
disabled={userStore.downloadKubectlBinaries}
/>
</section>
</>
);
},
);
export const KubectlBinaries = withInjectables<Dependencies>(
NonInjectedKubectlBinaries,
{
getProps: (di) => ({
defaultPathForGeneralBinaries: di.inject(directoryForBinariesInjectable),
defaultPathForKubectlBinaries: di.inject(directoryForKubectlBinariesInjectable),
userStore: di.inject(userStoreInjectable),
}),
},
);
@@ -0,0 +1,21 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import kubernetesPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/kubernetes/kubernetes-preferences-route.injectable";
import { Kubernetes } from "./kubernetes";
const kubernetesPreferencesRouteComponentInjectable = getInjectable({
id: "kubernetes-preferences-route-component",
instantiate: (di) => ({
route: di.inject(kubernetesPreferencesRouteInjectable),
Component: Kubernetes,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default kubernetesPreferencesRouteComponentInjectable;
@@ -8,24 +8,25 @@ import React from "react";
import { HelmCharts } from "./helm-charts";
import { KubeconfigSyncs } from "./kubeconfig-syncs";
import { KubectlBinaries } from "./kubectl-binaries";
import { Preferences } from "./preferences";
export const Kubernetes = observer(() => {
return (
export const Kubernetes = observer(() => (
<Preferences data-testid="kubernetes-preferences-page">
<section id="kubernetes">
<section id="kubectl">
<h2 data-testid="kubernetes-header">Kubernetes</h2>
<KubectlBinaries />
</section>
<hr/>
<hr />
<section id="kube-sync">
<h2 data-testid="kubernetes-sync-header">Kubeconfig Syncs</h2>
<KubeconfigSyncs />
</section>
<hr/>
<hr />
<section id="helm">
<h2>Helm Charts</h2>
<HelmCharts/>
<HelmCharts />
</section>
</section>
);
});
</Preferences>
));
@@ -0,0 +1,37 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import appPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/app/app-preferences-route.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const applicationPreferencesNavigationItemInjectable = getInjectable({
id: "application-preferences-navigation-item",
instantiate: (di) => {
const route = di.inject(appPreferencesRouteInjectable);
const navigateToPreferenceTab = di.inject(navigateToPreferenceTabInjectable);
const routeIsActive = di.inject(
routeIsActiveInjectable,
route,
);
return {
id: "application",
label: "App",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(() => true),
orderNumber: 10,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default applicationPreferencesNavigationItemInjectable;
@@ -0,0 +1,37 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import editorPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/editor/editor-preferences-route.injectable";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const editorPreferencesNavigationItemInjectable = getInjectable({
id: "editor-preferences-navigation-item",
instantiate: (di) => {
const route = di.inject(editorPreferencesRouteInjectable);
const navigateToPreferenceTab = di.inject(navigateToPreferenceTabInjectable);
const routeIsActive = di.inject(
routeIsActiveInjectable,
route,
);
return {
id: "editor",
label: "Editor",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(() => true),
orderNumber: 40,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default editorPreferencesNavigationItemInjectable;
@@ -0,0 +1,51 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import extensionsPreferenceItemsInjectable from "../extension-preference-items.injectable";
import extensionPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const extensionsPreferencesNavigationItemInjectable = getInjectable({
id: "extension-preferences-navigation-item",
instantiate: (di) => {
const preferenceItems = di.inject(
extensionsPreferenceItemsInjectable,
);
const navigateToPreferenceTab = di.inject(
navigateToPreferenceTabInjectable,
);
const route = di.inject(
extensionPreferencesRouteInjectable,
);
const routeIsActive = di.inject(
routeIsActiveInjectable,
route,
);
return {
id: "extensions",
label: "Extensions",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(
() => preferenceItems.get().length > 0,
),
orderNumber: 70,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default extensionsPreferencesNavigationItemInjectable;
@@ -0,0 +1,40 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import kubernetesPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/kubernetes/kubernetes-preferences-route.injectable";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const kubernetesPreferencesNavigationItemInjectable = getInjectable({
id: "kubernetes-preferences-navigation-item",
instantiate: (di) => {
const navigateToPreferenceTab = di.inject(navigateToPreferenceTabInjectable);
const route = di.inject(
kubernetesPreferencesRouteInjectable,
);
const routeIsActive = di.inject(
routeIsActiveInjectable,
route,
);
return {
id: "kubernetes",
label: "Kubernetes",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(() => true),
orderNumber: 30,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default kubernetesPreferencesNavigationItemInjectable;
@@ -0,0 +1,21 @@
/**
* 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 { Route } from "../../../../common/front-end-routing/route-injection-token";
import { navigateToRouteInjectionToken } from "../../../../common/front-end-routing/navigate-to-route-injection-token";
const navigateToPreferenceTabInjectable = getInjectable({
id: "navigate-to-preference-tab",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
return (route: Route) => () => {
navigateToRoute(route, { withoutAffectingBackButton: true });
};
},
});
export default navigateToPreferenceTabInjectable;
@@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import { filter, orderBy } from "lodash/fp";
import { computed, IComputedValue } from "mobx";
export const preferenceNavigationItemInjectionToken =
getInjectionToken<PreferenceNavigationItem>({
id: "preference-navigation-item-injection-token",
});
export interface PreferenceNavigationItem {
id: string;
label: string;
isActive: IComputedValue<boolean>;
isVisible: IComputedValue<boolean>;
navigate: () => void;
orderNumber: number;
}
const preferenceNavigationItemsInjectable = getInjectable({
id: "preference-navigation-items",
instantiate: (di) =>
computed((): PreferenceNavigationItem[] =>
pipeline(
di.injectMany(preferenceNavigationItemInjectionToken),
filter((item) => !!item.isVisible.get()),
(items) => orderBy([(item) => item.orderNumber], ["asc"], items),
),
),
});
export default preferenceNavigationItemsInjectable;
@@ -0,0 +1,61 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Tab, Tabs } from "../../tabs";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import React from "react";
import preferenceNavigationItemsInjectable, {
PreferenceNavigationItem,
} from "./preference-navigation-items.injectable";
import { observer } from "mobx-react";
interface Dependencies {
navigationItems: IComputedValue<PreferenceNavigationItem[]>;
}
const NonInjectedPreferencesNavigation = ({
navigationItems,
}: Dependencies) => (
<Tabs
className="flex column"
scrollable={false}
onChange={(item: PreferenceNavigationItem) => item.navigate()}
>
<div className="header">Preferences</div>
{navigationItems.get().map((item) => (
<PreferencesNavigationTab
key={item.id}
item={item}
data-testid={`tab-link-for-${item.id}`}
/>
))}
</Tabs>
);
interface PreferenceNavigationTabProps extends React.DOMAttributes<HTMLElement> {
item: PreferenceNavigationItem;
}
const PreferencesNavigationTab = observer(({ item }: PreferenceNavigationTabProps) => (
<Tab
value={item}
label={item.label}
data-testid={`tab-link-for-${item.id}`}
active={item.isActive.get()}
/>
));
export const PreferencesNavigation = withInjectables<Dependencies>(
NonInjectedPreferencesNavigation,
{
getProps: (di) => ({
navigationItems: di.inject(preferenceNavigationItemsInjectable),
}),
},
);
@@ -0,0 +1,37 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import proxyPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/proxy/proxy-preferences-route.injectable";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const proxyPreferencesNavigationItemInjectable = getInjectable({
id: "proxy-preferences-navigation-item",
instantiate: (di) => {
const route = di.inject(proxyPreferencesRouteInjectable);
const navigateToPreferenceTab = di.inject(
navigateToPreferenceTabInjectable,
);
const routeIsActive = di.inject(routeIsActiveInjectable, route);
return {
id: "proxy",
label: "Proxy",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(() => true),
orderNumber: 20,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default proxyPreferencesNavigationItemInjectable;
@@ -0,0 +1,49 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import telemetryPreferenceItemsInjectable from "../telemetry-preference-items.injectable";
import telemetryPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/telemetry/telemetry-preferences-route.injectable";
import sentryDnsUrlInjectable from "../sentry-dns-url.injectable";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const terminalPreferencesNavigationItemInjectable = getInjectable({
id: "telemetry-preferences-navigation-item",
instantiate: (di) => {
const sentryDnsUrl = di.inject(sentryDnsUrlInjectable);
const telemetryPreferenceItems = di.inject(
telemetryPreferenceItemsInjectable,
);
const navigateToPreferenceTab = di.inject(
navigateToPreferenceTabInjectable,
);
const route = di.inject(telemetryPreferencesRouteInjectable);
const routeIsActive = di.inject(routeIsActiveInjectable, route);
return {
id: "telemetry",
label: "Telemetry",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(
() => !!sentryDnsUrl || telemetryPreferenceItems.get().length > 0,
),
orderNumber: 60,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default terminalPreferencesNavigationItemInjectable;
@@ -0,0 +1,37 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import terminalPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/terminal/terminal-preferences-route.injectable";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const terminalPreferencesNavigationItemInjectable = getInjectable({
id: "terminal-preferences-navigation-item",
instantiate: (di) => {
const navigateToPreferenceTab = di.inject(
navigateToPreferenceTabInjectable,
);
const route = di.inject(terminalPreferencesRouteInjectable);
const routeIsActive = di.inject(routeIsActiveInjectable, route);
return {
id: "terminal",
label: "Terminal",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(() => true),
orderNumber: 50,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default terminalPreferencesNavigationItemInjectable;
@@ -3,99 +3,46 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./preferences.scss";
import type { IComputedValue } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import { matchPath, Redirect, Route, RouteProps, Switch } from "react-router";
import {
appRoute,
appURL,
editorURL,
extensionRoute,
extensionURL,
kubernetesRoute,
kubernetesURL,
preferencesURL,
proxyRoute,
proxyURL,
editorRoute,
telemetryRoute,
telemetryURL,
terminalRoute,
terminalURL,
} from "../../../common/routes";
import { navigateWithoutHistoryChange, navigation } from "../../navigation";
import { SettingLayout } from "../layout/setting-layout";
import { Tab, Tabs } from "../tabs";
import { Application } from "./application";
import { Kubernetes } from "./kubernetes";
import { Editor } from "./editor";
import { Terminal } from "./terminal";
import { LensProxy } from "./proxy";
import { Telemetry } from "./telemetry";
import { Extensions } from "./extensions";
import { sentryDsn } from "../../../common/vars";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
import appPreferencesInjectable from "./app-preferences/app-preferences.injectable";
interface Dependencies {
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>;
import { SettingLayout } from "../layout/setting-layout";
import { PreferencesNavigation } from "./preferences-navigation/preferences-navigation";
import { withInjectables } from "@ogre-tools/injectable-react";
import closePreferencesInjectable from "./close-preferences.injectable";
interface PreferencesProps extends React.DOMAttributes<any> {
children: React.ReactNode;
}
const NonInjectedPreferences: React.FC<Dependencies> = ({ appPreferenceItems }) => {
interface Dependencies {
closePreferences: () => void;
}
function renderNavigation() {
const extensions = appPreferenceItems.get();
const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == "telemetry");
const currentLocation = navigation.location.pathname;
const isActive = (route: RouteProps) => !!matchPath(currentLocation, { path: route.path, exact: route.exact });
const NonInjectedPreferences = ({
children,
closePreferences,
...props
}: PreferencesProps & Dependencies) => (
<SettingLayout
navigation={<PreferencesNavigation />}
className="Preferences"
contentGaps={false}
closeButtonProps={{ "data-testid": "close-preferences" }}
back={closePreferences}
{...props}
>
{children}
</SettingLayout>
);
return (
<Tabs className="flex column" scrollable={false} onChange={(url) => navigateWithoutHistoryChange({ pathname: url })}>
<div className="header">Preferences</div>
<Tab value={appURL()} label="Application" data-testid="application-tab" active={isActive(appRoute)}/>
<Tab value={proxyURL()} label="Proxy" data-testid="proxy-tab" active={isActive(proxyRoute)}/>
<Tab value={kubernetesURL()} label="Kubernetes" data-testid="kubernetes-tab" active={isActive(kubernetesRoute)}/>
<Tab value={editorURL()} label="Editor" data-testid="editor-tab" active={isActive(editorRoute)}/>
<Tab value={terminalURL()} label="Terminal" data-testid="terminal-tab" active={isActive(terminalRoute)}/>
{(telemetryExtensions.length > 0 || !!sentryDsn) &&
<Tab value={telemetryURL()} label="Telemetry" data-testid="telemetry-tab" active={isActive(telemetryRoute)}/>
}
{extensions.filter(e => !e.showInPreferencesTab).length > 0 &&
<Tab value={extensionURL()} label="Extensions" data-testid="extensions-tab" active={isActive(extensionRoute)}/>
}
</Tabs>
);
}
return (
<SettingLayout
navigation={renderNavigation()}
className="Preferences"
contentGaps={false}
>
<Switch>
<Route path={appURL()} component={Application}/>
<Route path={proxyURL()} component={LensProxy}/>
<Route path={kubernetesURL()} component={Kubernetes}/>
<Route path={editorURL()} component={Editor}/>
<Route path={terminalURL()} component={Terminal}/>
<Route path={telemetryURL()} component={Telemetry}/>
<Route path={extensionURL()} component={Extensions}/>
<Redirect exact from={`${preferencesURL()}/`} to={appURL()}/>
</Switch>
</SettingLayout>
);
};
export const Preferences = withInjectables<Dependencies>(
observer(NonInjectedPreferences),
export const Preferences = withInjectables<Dependencies, PreferencesProps>(
NonInjectedPreferences,
{
getProps: (di) => ({
appPreferenceItems: di.inject(appPreferencesInjectable),
getProps: (di, props) => ({
closePreferences: di.inject(closePreferencesInjectable),
...props,
}),
},
);
@@ -0,0 +1,21 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import proxyPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/proxy/proxy-preferences-route.injectable";
import { LensProxy } from "./proxy";
const proxyPreferencesRouteComponentInjectable = getInjectable({
id: "proxy-preferences-route-component",
instantiate: (di) => ({
route: di.inject(proxyPreferencesRouteInjectable),
Component: LensProxy,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default proxyPreferencesRouteComponentInjectable;
+50 -32
View File
@@ -2,48 +2,66 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { withInjectables } from "@ogre-tools/injectable-react";
import { observer } from "mobx-react";
import React from "react";
import { UserStore } from "../../../common/user-store";
import type { UserStore } from "../../../common/user-store";
import { Input } from "../input";
import { SubTitle } from "../layout/sub-title";
import { Switch } from "../switch";
import { Preferences } from "./preferences";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
export const LensProxy = observer(() => {
const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || "");
const store = UserStore.getInstance();
interface Dependencies {
userStore: UserStore;
}
const NonInjectedLensProxy = observer(({ userStore }: Dependencies) => {
const [proxy, setProxy] = React.useState(userStore.httpsProxy || "");
return (
<section id="proxy">
<section>
<h2 data-testid="proxy-header">Proxy</h2>
<SubTitle title="HTTP Proxy"/>
<Input
theme="round-black"
placeholder="Type HTTP proxy url (example: http://proxy.acme.org:8080)"
value={proxy}
onChange={v => setProxy(v)}
onBlur={() => UserStore.getInstance().httpsProxy = proxy}
/>
<small className="hint">
Proxy is used only for non-cluster communication.
</small>
</section>
<Preferences data-testid="proxy-preferences-page">
<section id="proxy">
<section>
<h2 data-testid="proxy-header">Proxy</h2>
<SubTitle title="HTTP Proxy" />
<Input
theme="round-black"
placeholder="Type HTTP proxy url (example: http://proxy.acme.org:8080)"
value={proxy}
onChange={v => setProxy(v)}
onBlur={() => userStore.httpsProxy = proxy}
/>
<small className="hint">
Proxy is used only for non-cluster communication.
</small>
</section>
<hr className="small"/>
<hr className="small" />
<section className="small">
<SubTitle title="Certificate Trust"/>
<Switch checked={store.allowUntrustedCAs} onChange={() => store.allowUntrustedCAs = !store.allowUntrustedCAs}>
Allow untrusted Certificate Authorities
</Switch>
<small className="hint">
This will make Lens to trust ANY certificate authority without any validations.{" "}
Needed with some corporate proxies that do certificate re-writing.{" "}
Does not affect cluster communications!
</small>
<section className="small">
<SubTitle title="Certificate Trust" />
<Switch checked={userStore.allowUntrustedCAs}
onChange={() => userStore.allowUntrustedCAs = !userStore.allowUntrustedCAs}>
Allow untrusted Certificate Authorities
</Switch>
<small className="hint">
This will make Lens to trust ANY certificate authority without any validations.{" "}
Needed with some corporate proxies that do certificate re-writing.{" "}
Does not affect cluster communications!
</small>
</section>
</section>
</section>
</Preferences>
);
});
export const LensProxy = withInjectables<Dependencies>(
NonInjectedLensProxy,
{
getProps: (di) => ({
userStore: di.inject(userStoreInjectable),
}),
},
);
@@ -0,0 +1,13 @@
/**
* 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 { sentryDsn } from "../../../common/vars";
const sentryDnsUrlInjectable = getInjectable({
id: "sentry-dns-url",
instantiate: () => sentryDsn,
});
export default sentryDnsUrlInjectable;
@@ -0,0 +1,50 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
import { pipeline } from "@ogre-tools/fp";
import { filter, overSome } from "lodash/fp";
import { computed } from "mobx";
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import type { AppPreferenceRegistration } from "./app-preferences/app-preference-registration";
export interface ExtensionTelemetryPreferenceRegistration extends AppPreferenceRegistration {
extension?: LensRendererExtension;
}
export const telemetryPreferenceItemInjectionToken = getInjectionToken<ExtensionTelemetryPreferenceRegistration>({
id: "telemetry-preference-item-injection-token",
});
const telemetryPreferenceItemsInjectable = getInjectable({
id: "telemetry-preference-items",
instantiate: (di) => {
const extensions = di.inject(rendererExtensionsInjectable);
return computed(() => {
const enabledExtensions = extensions.get();
return pipeline(
di.injectMany(telemetryPreferenceItemInjectionToken),
filter((item) =>
overSome([
isNonExtensionItem,
isEnabledExtensionItemFor(enabledExtensions),
])(item),
),
);
});
},
});
const isNonExtensionItem = (item: ExtensionTelemetryPreferenceRegistration) => !item.extension;
const isEnabledExtensionItemFor =
(enabledExtensions: LensRendererExtension[]) => (item: ExtensionTelemetryPreferenceRegistration) =>
!!enabledExtensions.find((extension) => extension === item.extension);
export default telemetryPreferenceItemsInjectable;
@@ -0,0 +1,21 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import telemetryPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/telemetry/telemetry-preferences-route.injectable";
import { Telemetry } from "./telemetry";
const telemetryPreferencesRouteComponentInjectable = getInjectable({
id: "telemetry-preferences-route-component",
instantiate: (di) => ({
route: di.inject(telemetryPreferencesRouteInjectable),
Component: Telemetry,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default telemetryPreferencesRouteComponentInjectable;
@@ -4,52 +4,67 @@
*/
import { observer } from "mobx-react";
import React from "react";
import { UserStore } from "../../../common/user-store";
import { sentryDsn } from "../../../common/vars";
import type { UserStore } from "../../../common/user-store";
import { Checkbox } from "../checkbox";
import { SubTitle } from "../layout/sub-title";
import { ExtensionSettings } from "./extension-settings";
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
import appPreferencesInjectable from "./app-preferences/app-preferences.injectable";
import type { IComputedValue } from "mobx";
import { withInjectables } from "@ogre-tools/injectable-react";
import { Preferences } from "./preferences";
import type { ExtensionTelemetryPreferenceRegistration } from "./telemetry-preference-items.injectable";
import telemetryPreferenceItemsInjectable from "./telemetry-preference-items.injectable";
import sentryDnsUrlInjectable from "./sentry-dns-url.injectable";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
interface Dependencies {
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>;
telemetryPreferenceItems: IComputedValue<ExtensionTelemetryPreferenceRegistration[]>;
sentryDnsUrl: string;
userStore: UserStore;
}
const NonInjectedTelemetry: React.FC<Dependencies> = ({ appPreferenceItems }) => {
const extensions = appPreferenceItems.get();
const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == "telemetry");
const NonInjectedTelemetry: React.FC<Dependencies> = ({
telemetryPreferenceItems,
sentryDnsUrl,
userStore,
}) => {
return (
<section id="telemetry">
<h2 data-testid="telemetry-header">Telemetry</h2>
{telemetryExtensions.map((extension) => <ExtensionSettings key={extension.id} setting={extension} size="small" />)}
{sentryDsn ? (
<React.Fragment key='sentry'>
<section id='sentry' className="small">
<SubTitle title='Automatic Error Reporting' />
<Checkbox
label="Allow automatic error reporting"
value={UserStore.getInstance().allowErrorReporting}
onChange={value => {
UserStore.getInstance().allowErrorReporting = value;
}}
/>
<div className="hint">
<span>
Automatic error reports provide vital information about issues and application crashes.
It is highly recommended to keep this feature enabled to ensure fast turnaround for issues you might encounter.
</span>
</div>
</section>
<hr className="small" />
</React.Fragment>) :
// we don't need to shows the checkbox at all if Sentry dsn is not a valid url
null
}
</section>
<Preferences data-testid="telemetry-preferences-page">
<section id="telemetry">
<h2 data-testid="telemetry-header">Telemetry</h2>
{telemetryPreferenceItems.get().map((item) => (
<ExtensionSettings
key={item.id}
setting={item}
size="small"
data-testid={`telemetry-preference-item-for-${item.id}`}
/>
))}
{sentryDnsUrl ? (
<React.Fragment key="sentry">
<section id="sentry" className="small" data-testid="telemetry-preferences-for-automatic-error-reporting">
<SubTitle title="Automatic Error Reporting" />
<Checkbox
label="Allow automatic error reporting"
value={userStore.allowErrorReporting}
onChange={(value) => {
userStore.allowErrorReporting = value;
}}
/>
<div className="hint">
<span>
Automatic error reports provide vital information about issues
and application crashes. It is highly recommended to keep this
feature enabled to ensure fast turnaround for issues you might
encounter.
</span>
</div>
</section>
<hr className="small" />
</React.Fragment>
) : // we don't need to shows the checkbox at all if Sentry dsn is not a valid url
null}
</section>
</Preferences>
);
};
@@ -58,7 +73,9 @@ export const Telemetry = withInjectables<Dependencies>(
{
getProps: (di) => ({
appPreferenceItems: di.inject(appPreferencesInjectable),
telemetryPreferenceItems: di.inject(telemetryPreferenceItemsInjectable),
sentryDnsUrl: di.inject(sentryDnsUrlInjectable),
userStore: di.inject(userStoreInjectable),
}),
},
);
@@ -0,0 +1,21 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import terminalPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/terminal/terminal-preferences-route.injectable";
import { Terminal } from "./terminal";
const terminalPreferencesRouteComponentInjectable = getInjectable({
id: "terminal-preferences-route-component",
instantiate: (di) => ({
route: di.inject(terminalPreferencesRouteInjectable),
Component: Terminal,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default terminalPreferencesRouteComponentInjectable;
@@ -5,80 +5,98 @@
import React from "react";
import { observer } from "mobx-react";
import { UserStore } from "../../../common/user-store";
import type { UserStore } from "../../../common/user-store";
import { SubTitle } from "../layout/sub-title";
import { Input, InputValidators } from "../input";
import { isWindows } from "../../../common/vars";
import { Switch } from "../switch";
import { Select } from "../select";
import { ThemeStore } from "../../theme.store";
import type { ThemeStore } from "../../theme.store";
import { Preferences } from "./preferences";
import { withInjectables } from "@ogre-tools/injectable-react";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import themeStoreInjectable from "../../theme-store.injectable";
import defaultShellInjectable from "./default-shell.injectable";
export const Terminal = observer(() => {
const userStore = UserStore.getInstance();
const themeStore = ThemeStore.getInstance();
const defaultShell = process.env.SHELL
|| process.env.PTYSHELL
|| (
isWindows
? "powershell.exe"
: "System default shell"
interface Dependencies {
userStore: UserStore;
themeStore: ThemeStore;
defaultShell: string;
}
const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: Dependencies) => {
return (
<Preferences data-testid="terminal-preferences-page">
<section>
<h2>Terminal</h2>
<section id="shell">
<SubTitle title="Terminal Shell Path" />
<Input
theme="round-black"
placeholder={defaultShell}
value={userStore.shell}
onChange={(value) => userStore.shell = value}
/>
</section>
<section id="terminalSelection">
<SubTitle title="Terminal copy & paste" />
<Switch
checked={userStore.terminalCopyOnSelect}
onChange={() => userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect}
>
Copy on select and paste on right-click
</Switch>
</section>
<section id="terminalTheme">
<SubTitle title="Terminal theme" />
<Select
id="terminal-theme-input"
themeName="lens"
options={[
{ label: "Match theme", value: "" },
...themeStore.themeOptions,
]}
value={userStore.terminalTheme}
onChange={({ value }) => userStore.terminalTheme = value}
/>
</section>
<section>
<SubTitle title="Font size" />
<Input
theme="round-black"
type="number"
min={10}
validators={InputValidators.isNumber}
value={userStore.terminalConfig.fontSize.toString()}
onChange={(value) => userStore.terminalConfig.fontSize = Number(value)}
/>
</section>
<section>
<SubTitle title="Font family" />
<Input
theme="round-black"
type="text"
value={userStore.terminalConfig.fontFamily}
onChange={(value) => userStore.terminalConfig.fontFamily = value}
/>
</section>
</section>
</Preferences>
);
return (<section>
<h2>Terminal</h2>
<section id="shell">
<SubTitle title="Terminal Shell Path"/>
<Input
theme="round-black"
placeholder={defaultShell}
value={userStore.shell}
onChange={(value) => userStore.shell = value}
/>
</section>
<section id="terminalSelection">
<SubTitle title="Terminal copy & paste" />
<Switch
checked={userStore.terminalCopyOnSelect}
onChange={() => userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect}
>
Copy on select and paste on right-click
</Switch>
</section>
<section id="terminalTheme">
<SubTitle title="Terminal theme" />
<Select
themeName="lens"
options={[
{ label: "Match theme", value: "" },
...themeStore.themeOptions,
]}
value={userStore.terminalTheme}
onChange={({ value }) => userStore.terminalTheme = value}
/>
</section>
<section>
<SubTitle title="Font size"/>
<Input
theme="round-black"
type="number"
min={10}
validators={InputValidators.isNumber}
value={userStore.terminalConfig.fontSize.toString()}
onChange={(value) => userStore.terminalConfig.fontSize=Number(value)}
/>
</section>
<section>
<SubTitle title="Font family"/>
<Input
theme="round-black"
type="text"
value={userStore.terminalConfig.fontFamily}
onChange={(value) => userStore.terminalConfig.fontFamily=value}
/>
</section>
</section>);
});
export const Terminal = withInjectables<Dependencies>(
NonInjectedTerminal,
{
getProps: (di) => ({
userStore: di.inject(userStoreInjectable),
themeStore: di.inject(themeStoreInjectable),
defaultShell: di.inject(defaultShellInjectable),
}),
},
);