From b7be3ae21a8f4766f9c26dbf90a8155d93d48d22 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Wed, 11 Aug 2021 13:24:33 +0300 Subject: [PATCH] Sync kubeconfigs from catalog (#3573) * Kubeconfig syncs from Catalog Signed-off-by: Alex Andreev * Allow to select directory to sync Signed-off-by: Alex Andreev * Convert Preferences to Route-based component Signed-off-by: Alex Andreev * Jump from notification to Preferences Signed-off-by: Alex Andreev * Adding navigateWithoutHistoryChange() method Signed-off-by: Alex Andreev * Cleaning up Signed-off-by: Alex Andreev * Fixing integration tests (no check for Telemetry tab) Signed-off-by: Alex Andreev * Fixing tests harder Signed-off-by: Alex Andreev --- integration/__tests__/app.tests.ts | 17 +- .../catalog-entities/kubernetes-cluster.ts | 24 +- src/common/catalog/catalog-entity.ts | 1 + src/common/routes/preferences.ts | 25 ++ src/common/user-store/user-store.ts | 2 +- src/common/utils/extended-map.ts | 6 + src/renderer/bootstrap.tsx | 1 + .../+catalog/catalog-add-button.tsx | 14 +- .../components/+preferences/application.tsx | 104 ++++++ .../components/+preferences/extensions.tsx | 38 +++ .../+preferences/kubeconfig-syncs.tsx | 60 ++-- .../+preferences/kubectl-binaries.tsx | 4 +- .../components/+preferences/kubernetes.tsx | 47 +++ .../components/+preferences/preferences.tsx | 301 ++++-------------- .../components/+preferences/proxy.tsx | 71 +++++ .../components/+preferences/telemetry.tsx | 63 ++++ .../components/layout/setting-layout.tsx | 12 +- .../components/path-picker/path-picker.tsx | 80 +++++ .../catalog-category-registry.tsx | 106 ++++++ src/renderer/initializers/index.ts | 1 + src/renderer/navigation/helpers.ts | 4 + src/renderer/theme.store.ts | 8 + 22 files changed, 672 insertions(+), 317 deletions(-) create mode 100644 src/renderer/components/+preferences/application.tsx create mode 100644 src/renderer/components/+preferences/extensions.tsx create mode 100644 src/renderer/components/+preferences/kubernetes.tsx create mode 100644 src/renderer/components/+preferences/proxy.tsx create mode 100644 src/renderer/components/+preferences/telemetry.tsx create mode 100644 src/renderer/components/path-picker/path-picker.tsx create mode 100644 src/renderer/initializers/catalog-category-registry.tsx diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 450f6bd039..0c28359317 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -56,14 +56,13 @@ describe("Lens integration tests", () => { await app.client.waitUntilTextExists("[data-testid=application-header]", "Application"); }); - it("shows all tabs and their contents", async () => { - await app.client.click("[data-testid=application-tab]"); - await app.client.click("[data-testid=proxy-tab]"); - await app.client.waitUntilTextExists("[data-testid=proxy-header]", "Proxy"); - await app.client.click("[data-testid=kube-tab]"); - await app.client.waitUntilTextExists("[data-testid=kubernetes-header]", "Kubernetes"); - await app.client.click("[data-testid=telemetry-tab]"); - await app.client.waitUntilTextExists("[data-testid=telemetry-header]", "Telemetry"); + it.each([ + ["application", "Application"], + ["proxy", "Proxy"], + ["kubernetes", "Kubernetes"], + ])("Can click the %s tab and see the %s header", async (tab, header) => { + await app.client.click(`[data-testid=${tab}-tab]`); + await app.client.waitUntilTextExists(`[data-testid=${tab}-header]`, header); }); it("ensures helm repos", async () => { @@ -73,7 +72,7 @@ describe("Lens integration tests", () => { fail("Lens failed to add any repositories"); } - await app.client.click("[data-testid=kube-tab]"); + await app.client.click("[data-testid=kubernetes-tab]"); await app.client.waitUntilTextExists("div.repos .repoName", repos[0].name); // wait for the helm-cli to fetch the repo(s) await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text) diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 2e8fdccf04..14a584ee8c 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -20,12 +20,11 @@ */ import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; -import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; +import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import { clusterActivateHandler, clusterDeleteHandler, clusterDisconnectHandler } from "../cluster-ipc"; import { ClusterStore } from "../cluster-store"; import { requestMain } from "../ipc"; import { CatalogCategory, CatalogCategorySpec } from "../catalog"; -import { addClusterURL } from "../routes"; import { app } from "electron"; import type { CatalogEntitySpec } from "../catalog/catalog-entity"; import { HotbarStore } from "../hotbar-store"; @@ -43,7 +42,6 @@ export interface KubernetesClusterPrometheusMetrics { export interface KubernetesClusterSpec extends CatalogEntitySpec { kubeconfigPath: string; kubeconfigContext: string; - accessibleNamespaces?: string[]; metrics?: { source: string; prometheus?: KubernetesClusterPrometheusMetrics; @@ -149,7 +147,7 @@ export class KubernetesCluster extends CatalogEntity { - ctx.menuItems.push({ - icon: "text_snippet", - title: "Add from kubeconfig", - onClick: () => { - ctx.navigate(addClusterURL()); - } - }); - }); - } } -catalogCategoryRegistry.add(new KubernetesClusterCategory()); +export const kubernetesClusterCategory = new KubernetesClusterCategory(); + +catalogCategoryRegistry.add(kubernetesClusterCategory); diff --git a/src/common/catalog/catalog-entity.ts b/src/common/catalog/catalog-entity.ts index bc3e360a7b..7b9db2f0ff 100644 --- a/src/common/catalog/catalog-entity.ts +++ b/src/common/catalog/catalog-entity.ts @@ -123,6 +123,7 @@ export interface CatalogEntityContextMenu { export interface CatalogEntityAddMenu extends CatalogEntityContextMenu { icon: string; + defaultAction?: boolean; } export interface CatalogEntitySettingsMenu { diff --git a/src/common/routes/preferences.ts b/src/common/routes/preferences.ts index 45fd3074c8..bae7e6257e 100644 --- a/src/common/routes/preferences.ts +++ b/src/common/routes/preferences.ts @@ -26,4 +26,29 @@ export const preferencesRoute: RouteProps = { path: "/preferences" }; +export const appRoute: RouteProps = { + path: `${preferencesRoute.path}/app` +}; + +export const proxyRoute: RouteProps = { + path: `${preferencesRoute.path}/proxy` +}; + +export const kubernetesRoute: RouteProps = { + path: `${preferencesRoute.path}/kubernetes` +}; + +export const telemetryRoute: RouteProps = { + path: `${preferencesRoute.path}/telemetry` +}; + +export const extensionRoute: RouteProps = { + path: `${preferencesRoute.path}/extensions` +}; + export const preferencesURL = buildURL(preferencesRoute.path); +export const appURL = buildURL(appRoute.path); +export const proxyURL = buildURL(proxyRoute.path); +export const kubernetesURL = buildURL(kubernetesRoute.path); +export const telemetryURL = buildURL(telemetryRoute.path); +export const extensionURL = buildURL(extensionRoute.path); diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index 7121357671..9cc5cd06e9 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -213,6 +213,6 @@ export class UserStore extends BaseStore /* implements UserStore * Getting default directory to download kubectl binaries * @returns string */ -export function getDefaultKubectlPath(): string { +export function getDefaultKubectlDownloadPath(): string { return path.join((app || remote.app).getPath("userData"), "binaries"); } diff --git a/src/common/utils/extended-map.ts b/src/common/utils/extended-map.ts index bd29cbbfe0..46621194e2 100644 --- a/src/common/utils/extended-map.ts +++ b/src/common/utils/extended-map.ts @@ -21,6 +21,12 @@ import { action, ObservableMap } from "mobx"; +export function multiSet(map: Map, newEntries: [T, V][]): void { + for (const [key, val] of newEntries) { + map.set(key, val); + } +} + export class ExtendedMap extends Map { static new(entries?: readonly (readonly [K, V])[] | null): ExtendedMap { return new ExtendedMap(entries); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 87c30dba59..3e7daa5404 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -81,6 +81,7 @@ export async function bootstrap(App: AppComponent) { initializers.initWelcomeMenuRegistry(); initializers.initWorkloadsOverviewDetailRegistry(); initializers.initCatalogEntityDetailRegistry(); + initializers.initCatalogCategoryRegistryEntries(); initializers.initCatalog(); initializers.initIpcRendererListeners(); diff --git a/src/renderer/components/+catalog/catalog-add-button.tsx b/src/renderer/components/+catalog/catalog-add-button.tsx index 25cd43239f..8f3cc20412 100644 --- a/src/renderer/components/+catalog/catalog-add-button.tsx +++ b/src/renderer/components/+catalog/catalog-add-button.tsx @@ -73,9 +73,10 @@ export class CatalogAddButton extends React.Component { @boundMethod onButtonClick() { - if (this.menuItems.length == 1) { - this.menuItems[0].onClick(); - } + const defaultAction = this.menuItems.find(item => item.defaultAction)?.onClick; + const clickAction = defaultAction || (this.menuItems.length === 1 ? this.menuItems[0].onClick : null); + + clickAction?.(); } render() { @@ -97,9 +98,12 @@ export class CatalogAddButton extends React.Component { { this.menuItems.map((menuItem, index) => { return } + icon={} tooltipTitle={menuItem.title} - onClick={() => menuItem.onClick()} + onClick={(evt) => { + evt.stopPropagation(); + menuItem.onClick(); + }} TooltipClasses={{ popper: "catalogSpeedDialPopper" }} diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx new file mode 100644 index 0000000000..ac05012f2f --- /dev/null +++ b/src/renderer/components/+preferences/application.tsx @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +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 { Input } from "../input"; +import { isWindows } from "../../../common/vars"; +import { FormSwitch, Switcher } from "../switch"; +import moment from "moment-timezone"; + +const timezoneOptions: SelectOption[] = moment.tz.names().map(zone => ({ + label: zone, + value: zone, +})); + +export const Application = observer(() => { + const defaultShell = process.env.SHELL + || process.env.PTYSHELL + || ( + isWindows + ? "powershell.exe" + : "System default shell" + ); + + const [shell, setShell] = React.useState(UserStore.getInstance().shell || ""); + + return ( +
+

Application

+
+ + setShell(v)} + onBlur={() => UserStore.getInstance().shell = shell} + /> +
+ +
+ +
+ + UserStore.getInstance().openAtLogin = v.target.checked} + name="startup" + /> + } + label="Automatically start Lens on login" + /> +
+ +
+ +
+ + { + return ( +
+
+

Kubernetes

+ +
+
+
+

Kubeconfig Syncs

+ +
+
+
+

Helm Charts

+ +
+
+ ); +}); diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx index f384d049db..c96959de8b 100644 --- a/src/renderer/components/+preferences/preferences.tsx +++ b/src/renderer/components/+preferences/preferences.tsx @@ -18,279 +18,100 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - import "./preferences.scss"; +import { makeObservable, observable } from "mobx"; +import { observer } from "mobx-react"; import React from "react"; -import moment from "moment-timezone"; -import { computed, observable, reaction, makeObservable } from "mobx"; -import { disposeOnUnmount, observer } from "mobx-react"; +import { matchPath, Redirect, Route, RouteProps, Switch } from "react-router"; -import { isWindows } from "../../../common/vars"; +import { + appRoute, + appURL, + extensionRoute, + extensionURL, + kubernetesRoute, + kubernetesURL, + preferencesURL, + proxyRoute, + proxyURL, + telemetryRoute, + telemetryURL, +} from "../../../common/routes"; import { AppPreferenceRegistry, RegisteredAppPreference } from "../../../extensions/registries/app-preference-registry"; -import { UserStore } from "../../../common/user-store"; -import { ThemeStore } from "../../theme.store"; -import { Input } from "../input"; -import { SubTitle } from "../layout/sub-title"; -import { Select, SelectOption } from "../select"; -import { HelmCharts } from "./helm-charts"; -import { KubectlBinaries } from "./kubectl-binaries"; -import { navigation } from "../../navigation"; -import { Tab, Tabs } from "../tabs"; -import { FormSwitch, Switcher } from "../switch"; -import { KubeconfigSyncs } from "./kubeconfig-syncs"; +import { navigateWithoutHistoryChange, navigation } from "../../navigation"; import { SettingLayout } from "../layout/setting-layout"; -import { Checkbox } from "../checkbox"; +import { SubTitle } from "../layout/sub-title"; +import { Tab, Tabs } from "../tabs"; +import { Application } from "./application"; +import { Kubernetes } from "./kubernetes"; +import { LensProxy } from "./proxy"; +import { Telemetry } from "./telemetry"; +import { Extensions } from "./extensions"; import { sentryDsn } from "../../../common/vars"; -enum Pages { - Application = "application", - Proxy = "proxy", - Kubernetes = "kubernetes", - Telemetry = "telemetry", - Extensions = "extensions", - Other = "other" -} - @observer export class Preferences extends React.Component { - @observable httpProxy = UserStore.getInstance().httpsProxy || ""; - @observable shell = UserStore.getInstance().shell || ""; - @observable activeTab = Pages.Application; + @observable historyLength: number | undefined; constructor(props: {}) { super(props); makeObservable(this); } - @computed get themeOptions(): SelectOption[] { - return ThemeStore.getInstance().themes.map(theme => ({ - label: theme.name, - value: theme.id, - })); - } - - timezoneOptions: SelectOption[] = moment.tz.names().map(zone => ({ - label: zone, - value: zone, - })); - - componentDidMount() { - disposeOnUnmount(this, [ - reaction(() => navigation.location.hash, hash => { - const fragment = hash.slice(1); // hash is /^(#\w.)?$/ - - if (fragment) { - // ignore empty fragments - document.getElementById(fragment)?.scrollIntoView(); - } - }, { - fireImmediately: true - }) - ]); - } - - onTabChange = (tabId: Pages) => { - this.activeTab = tabId; - }; - renderNavigation() { - const extensions = AppPreferenceRegistry.getInstance().getItems().filter(e => !e.showInPreferencesTab); + const extensions = AppPreferenceRegistry.getInstance().getItems(); + 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 }); return ( - + navigateWithoutHistoryChange({ pathname: url })}>
Preferences
- - - - - {extensions.length > 0 && - + + + + {telemetryExtensions.length > 0 || !!sentryDsn && + + } + {extensions.filter(e => !e.showInPreferencesTab).length > 0 && + }
); } - renderExtension({ title, id, components: { Hint, Input } }: RegisteredAppPreference) { - return ( - -
- - -
- -
-
-
-
- ); - } - render() { - const extensions = AppPreferenceRegistry.getInstance().getItems(); - const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == Pages.Telemetry); - const defaultShell = process.env.SHELL - || process.env.PTYSHELL - || ( - isWindows - ? "powershell.exe" - : "System default shell" - ); - return ( - {this.activeTab == Pages.Application && ( -
-

Application

-
- - this.shell = v} - onBlur={() => UserStore.getInstance().shell = this.shell} - /> -
- -
- -
- - UserStore.getInstance().openAtLogin = v.target.checked} - name="startup" - /> - } - label="Automatically start Lens on login" - /> -
- -
- -
- - this.httpProxy = v} - onBlur={() => UserStore.getInstance().httpsProxy = this.httpProxy} - /> - - Proxy is used only for non-cluster communication. - -
- -
- -
- - UserStore.getInstance().allowUntrustedCAs = v.target.checked} - name="startup" - /> - } - label="Allow untrusted Certificate Authorities" - /> - - 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! - -
-
- )} - {this.activeTab == Pages.Kubernetes && ( -
-
-

Kubernetes

- -
-
-
-

Kubeconfig Syncs

- -
-
-
-

Helm Charts

- -
-
- )} - {this.activeTab == Pages.Telemetry && ( -
-

Telemetry

- {telemetryExtensions.map(this.renderExtension)} - {sentryDsn ? ( - -
- - { - UserStore.getInstance().allowErrorReporting = value; - }} - /> -
- - 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. - -
-
-
-
) : - // we don't need to shows the checkbox at all if Sentry dsn is not a valid url - null - } -
- )} - {this.activeTab == Pages.Extensions && ( -
-

Extensions

- {extensions.filter(e => !e.showInPreferencesTab).map(this.renderExtension)} -
- )} + + + + + + + +
); } } + +export function ExtensionSettings({ title, id, components: { Hint, Input } }: RegisteredAppPreference) { + return ( + +
+ + +
+ +
+
+
+
+ ); +} diff --git a/src/renderer/components/+preferences/proxy.tsx b/src/renderer/components/+preferences/proxy.tsx new file mode 100644 index 0000000000..b85bd5e0e1 --- /dev/null +++ b/src/renderer/components/+preferences/proxy.tsx @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { observer } from "mobx-react"; +import React from "react"; + +import { UserStore } from "../../../common/user-store"; +import { Input } from "../input"; +import { SubTitle } from "../layout/sub-title"; +import { FormSwitch, Switcher } from "../switch"; + +export const LensProxy = observer(() => { + const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || ""); + + return ( +
+
+

Proxy

+ + setProxy(v)} + onBlur={() => UserStore.getInstance().httpsProxy = proxy} + /> + + Proxy is used only for non-cluster communication. + +
+ +
+ +
+ + UserStore.getInstance().allowUntrustedCAs = v.target.checked} + name="startup" + /> + } + label="Allow untrusted Certificate Authorities" + /> + + 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! + +
+
+ ); +}); diff --git a/src/renderer/components/+preferences/telemetry.tsx b/src/renderer/components/+preferences/telemetry.tsx new file mode 100644 index 0000000000..5414382f92 --- /dev/null +++ b/src/renderer/components/+preferences/telemetry.tsx @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { observer } from "mobx-react"; +import React from "react"; +import { UserStore } from "../../../common/user-store"; +import { sentryDsn } from "../../../common/vars"; +import { AppPreferenceRegistry } from "../../../extensions/registries"; +import { Checkbox } from "../checkbox"; +import { SubTitle } from "../layout/sub-title"; +import { ExtensionSettings } from "./preferences"; + +export const Telemetry = observer(() => { + const extensions = AppPreferenceRegistry.getInstance().getItems(); + const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == "telemetry"); + + return ( +
+

Telemetry

+ {telemetryExtensions.map((extension) => )} + {sentryDsn ? ( + +
+ + { + UserStore.getInstance().allowErrorReporting = value; + }} + /> +
+ + 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. + +
+
+
+
) : + // we don't need to shows the checkbox at all if Sentry dsn is not a valid url + null + } +
+ ); +}); diff --git a/src/renderer/components/layout/setting-layout.tsx b/src/renderer/components/layout/setting-layout.tsx index 53e54e07ac..45f665df1c 100644 --- a/src/renderer/components/layout/setting-layout.tsx +++ b/src/renderer/components/layout/setting-layout.tsx @@ -36,6 +36,14 @@ export interface SettingLayoutProps extends React.DOMAttributes { back?: (evt: React.MouseEvent | KeyboardEvent) => void; } +function scrollToAnchor() { + const { hash } = window.location; + + if (hash) { + document.querySelector(`${hash}`).scrollIntoView(); + } +} + const defaultProps: Partial = { provideBackButtonNavigation: true, contentGaps: true, @@ -59,6 +67,8 @@ export class SettingLayout extends React.Component { async componentDidMount() { window.addEventListener("keydown", this.onEscapeKey); + + scrollToAnchor(); } componentWillUnmount() { @@ -79,7 +89,7 @@ export class SettingLayout extends React.Component { render() { const { contentClass, provideBackButtonNavigation, - contentGaps, navigation, children, ...elemProps + contentGaps, navigation, children, back, ...elemProps } = this.props; const className = cssNames("SettingLayout", { showNavigation: navigation }, this.props.className); diff --git a/src/renderer/components/path-picker/path-picker.tsx b/src/renderer/components/path-picker/path-picker.tsx new file mode 100644 index 0000000000..782b0e67d5 --- /dev/null +++ b/src/renderer/components/path-picker/path-picker.tsx @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { FileFilter, OpenDialogOptions, remote } from "electron"; +import { observer } from "mobx-react"; +import React from "react"; +import { cssNames } from "../../utils"; +import { Button } from "../button"; + +export interface PathPickOpts { + label: string; + onPick?: (paths: string[]) => any; + onCancel?: () => any; + defaultPath?: string; + buttonLabel?: string; + filters?: FileFilter[]; + properties?: OpenDialogOptions["properties"]; + securityScopedBookmarks?: boolean; +} + +export interface PathPickerProps extends PathPickOpts { + className?: string; + disabled?: boolean; +} + +@observer +export class PathPicker extends React.Component { + static async pick(opts: PathPickOpts) { + const { onPick, onCancel, label, ...dialogOptions } = opts; + const { dialog, BrowserWindow } = remote; + const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), { + message: label, + ...dialogOptions, + }); + + if (canceled) { + await onCancel?.(); + } else { + await onPick?.(filePaths); + } + } + + async onClick() { + const { className, disabled, ...pickOpts } = this.props; + + return PathPicker.pick(pickOpts); + } + + render() { + const { className, label, disabled } = this.props; + + return ( +