mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Introduce clearer boundry between extensions (#7164)
- Bundled extensions are always enabled, and are always compatible - Have bundled extensions be loaded asyncronously to support typescript dynamic import (which is typed) as opposed to require Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
1 parent
351f9d492f
commit
058494bc73
29 files changed
+440
-550
No files matched your search
+1
-1
@@ -86,7 +86,7 @@ const attemptInstall = ({
|
||||
}
|
||||
|
||||
const extensionFolder = getExtensionDestFolder(name);
|
||||
const installedExtension = extensionLoader.getExtension(validatedRequest.id);
|
||||
const installedExtension = extensionLoader.getExtensionById(validatedRequest.id);
|
||||
|
||||
if (installedExtension) {
|
||||
const { version: oldVersion } = installedExtension.manifest;
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ const unpackExtensionInjectable = getInjectable({
|
||||
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
||||
|
||||
// wait for the loader has actually install it
|
||||
await when(() => extensionLoader.userExtensions.has(id));
|
||||
await when(() => extensionLoader.userExtensions.get().has(id));
|
||||
|
||||
// Enable installed extensions by default.
|
||||
extensionLoader.setIsEnabled(id, true);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||
|
||||
export type DisableExtension = (extId: LensExtensionId) => void;
|
||||
|
||||
const disableExtensionInjectable = getInjectable({
|
||||
id: "disable-extension",
|
||||
|
||||
instantiate: (di): DisableExtension => {
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
|
||||
return (extId) => {
|
||||
const ext = extensionLoader.getExtensionById(extId);
|
||||
|
||||
if (ext && !ext.isBundled) {
|
||||
ext.isEnabled = false;
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default disableExtensionInjectable;
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* 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 extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import { disableExtension } from "./disable-extension";
|
||||
|
||||
const disableExtensionInjectable = getInjectable({
|
||||
id: "disable-extension",
|
||||
|
||||
instantiate: (di) =>
|
||||
disableExtension({
|
||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default disableExtensionInjectable;
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
}
|
||||
|
||||
export const disableExtension =
|
||||
({ extensionLoader }: Dependencies) =>
|
||||
(id: LensExtensionId) => {
|
||||
const extension = extensionLoader.getExtension(id);
|
||||
|
||||
if (extension) {
|
||||
extension.isEnabled = false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
|
||||
|
||||
export type EnableExtension = (extId: LensExtensionId) => void;
|
||||
|
||||
const enableExtensionInjectable = getInjectable({
|
||||
id: "enable-extension",
|
||||
|
||||
instantiate: (di): EnableExtension => {
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
|
||||
return (extId) => {
|
||||
const ext = extensionLoader.getExtensionById(extId);
|
||||
|
||||
if (ext && !ext.isBundled) {
|
||||
ext.isEnabled = true;
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default enableExtensionInjectable;
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* 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 extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import { enableExtension } from "./enable-extension";
|
||||
|
||||
const enableExtensionInjectable = getInjectable({
|
||||
id: "enable-extension",
|
||||
|
||||
instantiate: (di) =>
|
||||
enableExtension({
|
||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default enableExtensionInjectable;
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
}
|
||||
|
||||
export const enableExtension =
|
||||
({ extensionLoader }: Dependencies) =>
|
||||
(id: LensExtensionId) => {
|
||||
const extension = extensionLoader.getExtension(id);
|
||||
|
||||
if (extension) {
|
||||
extension.isEnabled = true;
|
||||
}
|
||||
};
|
||||
@@ -4,138 +4,66 @@
|
||||
*/
|
||||
|
||||
import styles from "./extensions.module.scss";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import {
|
||||
makeObservable,
|
||||
observable,
|
||||
reaction,
|
||||
when,
|
||||
} from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { DropFileInput } from "../input";
|
||||
import { Install } from "./install";
|
||||
import { ExtensionInstall } from "./install";
|
||||
import { InstalledExtensions } from "./installed-extensions";
|
||||
import { Notice } from "./notice";
|
||||
import { SettingLayout } from "../layout/setting-layout";
|
||||
import { docsUrl } from "../../../common/vars";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
|
||||
import userExtensionsInjectable from "./user-extensions/user-extensions.injectable";
|
||||
import enableExtensionInjectable from "./enable-extension/enable-extension.injectable";
|
||||
import disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
|
||||
import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable";
|
||||
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable";
|
||||
import type { InstallExtensionFromInput } from "./install-extension-from-input.injectable";
|
||||
import installExtensionFromInputInjectable from "./install-extension-from-input.injectable";
|
||||
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
|
||||
import type { InstallOnDrop } from "./install-on-drop.injectable";
|
||||
import installOnDropInjectable from "./install-on-drop.injectable";
|
||||
import { supportedExtensionFormats } from "./supported-extension-formats";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import Gutter from "../gutter/gutter";
|
||||
import type { InstalledExtension, LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
|
||||
const ExtensionsNotice = () => (
|
||||
<Notice className={styles.notice}>
|
||||
<p>
|
||||
{"Add new features via Lens Extensions. Check out the "}
|
||||
<a
|
||||
href={`${docsUrl}/extensions/lens-extensions`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
docs
|
||||
</a>
|
||||
{" and list of "}
|
||||
<a
|
||||
href="https://github.com/lensapp/lens-extensions/blob/main/README.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
available extensions
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</Notice>
|
||||
);
|
||||
|
||||
interface Dependencies {
|
||||
userExtensions: IComputedValue<InstalledExtension[]>;
|
||||
enableExtension: (id: LensExtensionId) => void;
|
||||
disableExtension: (id: LensExtensionId) => void;
|
||||
confirmUninstallExtension: ConfirmUninstallExtension;
|
||||
installExtensionFromInput: InstallExtensionFromInput;
|
||||
installFromSelectFileDialog: () => Promise<void>;
|
||||
installOnDrop: InstallOnDrop;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedExtensions extends React.Component<Dependencies> {
|
||||
@observable installPath = "";
|
||||
|
||||
constructor(props: Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.userExtensions.get().length, (curSize, prevSize) => {
|
||||
if (curSize > prevSize) {
|
||||
disposeOnUnmount(this, [
|
||||
when(() => !this.props.extensionInstallationStateStore.anyInstalling, () => this.installPath = ""),
|
||||
]);
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
render() {
|
||||
const userExtensions = this.props.userExtensions.get();
|
||||
|
||||
return (
|
||||
<DropFileInput onDropFiles={this.props.installOnDrop}>
|
||||
<SettingLayout
|
||||
className="Extensions"
|
||||
contentGaps={false}
|
||||
data-testid="extensions-page"
|
||||
>
|
||||
<section>
|
||||
<h1>Extensions</h1>
|
||||
|
||||
<Notice className={styles.notice}>
|
||||
<p>
|
||||
{"Add new features via Lens Extensions. Check out the "}
|
||||
<a
|
||||
href={`${docsUrl}/extensions/`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
docs
|
||||
</a>
|
||||
{" and list of "}
|
||||
<a
|
||||
href="https://github.com/lensapp/lens-extensions/blob/main/README.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
available extensions
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</Notice>
|
||||
|
||||
<Install
|
||||
supportedFormats={supportedExtensionFormats}
|
||||
onChange={value => (this.installPath = value)}
|
||||
installFromInput={() => this.props.installExtensionFromInput(this.installPath)}
|
||||
installFromSelectFileDialog={this.props.installFromSelectFileDialog}
|
||||
installPath={this.installPath}
|
||||
/>
|
||||
|
||||
<Gutter size="md" />
|
||||
|
||||
<InstalledExtensions
|
||||
extensions={userExtensions}
|
||||
enable={this.props.enableExtension}
|
||||
disable={this.props.disableExtension}
|
||||
uninstall={this.props.confirmUninstallExtension}
|
||||
/>
|
||||
</section>
|
||||
</SettingLayout>
|
||||
</DropFileInput>
|
||||
);
|
||||
}
|
||||
}
|
||||
const NonInjectedExtensions = ({ installOnDrop }: Dependencies) => (
|
||||
<DropFileInput onDropFiles={installOnDrop}>
|
||||
<SettingLayout
|
||||
className="Extensions"
|
||||
contentGaps={false}
|
||||
data-testid="extensions-page"
|
||||
>
|
||||
<section>
|
||||
<h1>Extensions</h1>
|
||||
<ExtensionsNotice />
|
||||
<ExtensionInstall />
|
||||
<Gutter size="md" />
|
||||
<InstalledExtensions />
|
||||
</section>
|
||||
</SettingLayout>
|
||||
</DropFileInput>
|
||||
);
|
||||
|
||||
export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
|
||||
getProps: (di) => ({
|
||||
userExtensions: di.inject(userExtensionsInjectable),
|
||||
enableExtension: di.inject(enableExtensionInjectable),
|
||||
disableExtension: di.inject(disableExtensionInjectable),
|
||||
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
|
||||
installExtensionFromInput: di.inject(installExtensionFromInputInjectable),
|
||||
installOnDrop: di.inject(installOnDropInjectable),
|
||||
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import styles from "./install.module.scss";
|
||||
import React from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { prevDefault } from "@k8slens/utilities";
|
||||
import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
@@ -16,17 +16,16 @@ import type { ExtensionInstallationStateStore } from "../../../extensions/extens
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import { unionInputValidatorsAsync } from "../input/input_validators";
|
||||
|
||||
export interface InstallProps {
|
||||
installPath: string;
|
||||
supportedFormats: string[];
|
||||
onChange: (path: string) => void;
|
||||
installFromInput: () => void;
|
||||
installFromSelectFileDialog: () => void;
|
||||
}
|
||||
import { supportedExtensionFormats } from "./supported-extension-formats";
|
||||
import type { InstallExtensionFromInput } from "./install-extension-from-input.injectable";
|
||||
import type { InstallFromSelectFileDialog } from "./install-from-select-file-dialog.injectable";
|
||||
import installExtensionFromInputInjectable from "./install-extension-from-input.injectable";
|
||||
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
installState: ExtensionInstallationStateStore;
|
||||
installExtensionFromInput: InstallExtensionFromInput;
|
||||
installFromSelectFileDialog: InstallFromSelectFileDialog;
|
||||
}
|
||||
|
||||
const installInputValidator = unionInputValidatorsAsync(
|
||||
@@ -38,71 +37,75 @@ const installInputValidator = unionInputValidatorsAsync(
|
||||
InputValidators.isPath,
|
||||
);
|
||||
|
||||
const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({
|
||||
installPath,
|
||||
supportedFormats,
|
||||
onChange,
|
||||
installFromInput,
|
||||
const installTitle = `Name or file path or URL to an extension package (${supportedExtensionFormats.join(", ")})`;
|
||||
|
||||
const NonInjectedInstall = observer(({
|
||||
installExtensionFromInput,
|
||||
installFromSelectFileDialog,
|
||||
extensionInstallationStateStore,
|
||||
}) => (
|
||||
<section>
|
||||
<SubTitle
|
||||
title={`Name or file path or URL to an extension package (${supportedFormats.join(
|
||||
", ",
|
||||
)})`}
|
||||
/>
|
||||
<div className={styles.inputs}>
|
||||
<div>
|
||||
<Input
|
||||
theme="round-black"
|
||||
disabled={extensionInstallationStateStore.anyPreInstallingOrInstalling}
|
||||
placeholder={"Name or file path or URL"}
|
||||
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
||||
validators={installPath ? installInputValidator : undefined}
|
||||
value={installPath}
|
||||
onChange={onChange}
|
||||
onSubmit={installFromInput}
|
||||
iconRight={(
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
smallest
|
||||
material="folder_open"
|
||||
onClick={prevDefault(installFromSelectFileDialog)}
|
||||
tooltip="Browse"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className={styles.button}
|
||||
primary
|
||||
label="Install"
|
||||
disabled={
|
||||
extensionInstallationStateStore.anyPreInstallingOrInstalling
|
||||
}
|
||||
waiting={extensionInstallationStateStore.anyPreInstallingOrInstalling}
|
||||
onClick={installFromInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<small className={styles.proTip}>
|
||||
<b>Pro-Tip</b>
|
||||
: you can drag and drop a tarball file to this area
|
||||
</small>
|
||||
</section>
|
||||
);
|
||||
installState,
|
||||
}: Dependencies) => {
|
||||
const [installPath, setInstallPath] = useState("");
|
||||
const prevAnyInstalling = useRef(installState.anyInstalling);
|
||||
|
||||
export const Install = withInjectables<Dependencies, InstallProps>(
|
||||
observer(NonInjectedInstall),
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
extensionInstallationStateStore: di.inject(
|
||||
extensionInstallationStateStoreInjectable,
|
||||
),
|
||||
useEffect(() => {
|
||||
const currentlyInstalling = installState.anyInstalling;
|
||||
const previouslyInstalling = prevAnyInstalling.current;
|
||||
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!currentlyInstalling && previouslyInstalling) {
|
||||
prevAnyInstalling.current = false;
|
||||
setInstallPath("");
|
||||
}
|
||||
}, [installState.anyInstalling]);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SubTitle title={installTitle} />
|
||||
<div className={styles.inputs}>
|
||||
<div>
|
||||
<Input
|
||||
theme="round-black"
|
||||
disabled={installState.anyPreInstallingOrInstalling}
|
||||
placeholder="Name or file path or URL"
|
||||
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
||||
validators={installPath ? installInputValidator : undefined}
|
||||
value={installPath}
|
||||
onChange={setInstallPath}
|
||||
onSubmit={() => installExtensionFromInput(installPath)}
|
||||
iconRight={(
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
smallest
|
||||
material="folder_open"
|
||||
onClick={prevDefault(installFromSelectFileDialog)}
|
||||
tooltip="Browse"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className={styles.button}
|
||||
primary
|
||||
label="Install"
|
||||
disabled={installState.anyPreInstallingOrInstalling}
|
||||
waiting={installState.anyPreInstallingOrInstalling}
|
||||
onClick={() => installExtensionFromInput(installPath)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<small className={styles.proTip}>
|
||||
<b>Pro-Tip</b>
|
||||
: you can drag and drop a tarball file to this area
|
||||
</small>
|
||||
</section>
|
||||
);
|
||||
});
|
||||
|
||||
export const ExtensionInstall = withInjectables<Dependencies>(NonInjectedInstall, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
installState: di.inject(extensionInstallationStateStoreInjectable),
|
||||
installExtensionFromInput: di.inject(installExtensionFromInputInjectable),
|
||||
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -4,8 +4,7 @@
|
||||
*/
|
||||
|
||||
import styles from "./installed-extensions.module.scss";
|
||||
import React, { useMemo } from "react";
|
||||
import type { ExtensionDiscovery } from "../../../extensions/extension-discovery/extension-discovery";
|
||||
import React from "react";
|
||||
import { Icon } from "../icon";
|
||||
import { List } from "../list/list";
|
||||
import { MenuActions, MenuItem } from "../menu";
|
||||
@@ -17,18 +16,27 @@ import extensionDiscoveryInjectable from "../../../extensions/extension-discover
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import type { InstalledExtension, LensExtensionId } from "@k8slens/legacy-extensions";
|
||||
import type { InstalledExtension } from "@k8slens/legacy-extensions";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable";
|
||||
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable";
|
||||
import type { DisableExtension } from "./disable-extension.injectable";
|
||||
import disableExtensionInjectable from "./disable-extension.injectable";
|
||||
import type { EnableExtension } from "./enable-extension.injectable";
|
||||
import enableExtensionInjectable from "./enable-extension.injectable";
|
||||
import userExtensionsInjectable from "./user-extensions/user-extensions.injectable";
|
||||
import type { ExtensionDiscovery } from "../../../extensions/extension-discovery/extension-discovery";
|
||||
|
||||
export interface InstalledExtensionsProps {
|
||||
extensions: InstalledExtension[];
|
||||
enable: (id: LensExtensionId) => void;
|
||||
disable: (id: LensExtensionId) => void;
|
||||
uninstall: (extension: InstalledExtension) => void;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
extensionDiscovery: ExtensionDiscovery;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
userExtensions: IComputedValue<InstalledExtension[]>;
|
||||
enableExtension: EnableExtension;
|
||||
disableExtension: DisableExtension;
|
||||
confirmUninstallExtension: ConfirmUninstallExtension;
|
||||
}
|
||||
|
||||
function getStatus(extension: InstalledExtension) {
|
||||
@@ -39,106 +47,20 @@ function getStatus(extension: InstalledExtension) {
|
||||
return extension.isEnabled ? "Enabled" : "Disabled";
|
||||
}
|
||||
|
||||
const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extensionInstallationStateStore, extensions, uninstall, enable, disable }: Dependencies & InstalledExtensionsProps) => {
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: "Name",
|
||||
accessor: "extension",
|
||||
width: 200,
|
||||
sortType: (rowA: Row, rowB: Row) => { // Custom sorting for extension name
|
||||
const nameA = extensions[rowA.index].manifest.name;
|
||||
const nameB = extensions[rowB.index].manifest.name;
|
||||
|
||||
if (nameA > nameB) return -1;
|
||||
if (nameB > nameA) return 1;
|
||||
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Version",
|
||||
accessor: "version",
|
||||
},
|
||||
{
|
||||
Header: "Status",
|
||||
accessor: "status",
|
||||
},
|
||||
{
|
||||
Header: "",
|
||||
accessor: "actions",
|
||||
disableSortBy: true,
|
||||
width: 20,
|
||||
className: "actions",
|
||||
},
|
||||
], [],
|
||||
);
|
||||
|
||||
const data = useMemo(
|
||||
() => extensions.map(extension => {
|
||||
const { id, isEnabled, isCompatible, manifest } = extension;
|
||||
const { name, description, version } = manifest;
|
||||
const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id);
|
||||
|
||||
return {
|
||||
extension: (
|
||||
<div className={"flex items-start"}>
|
||||
<div>
|
||||
<div className={styles.extensionName}>{name}</div>
|
||||
<div className={styles.extensionDescription}>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
version,
|
||||
status: (
|
||||
<div className={cssNames({ [styles.enabled]: isEnabled, [styles.invalid]: !isCompatible })}>
|
||||
{getStatus(extension)}
|
||||
</div>
|
||||
),
|
||||
actions: (
|
||||
<MenuActions
|
||||
id={`menu-actions-for-installed-extensions-for-${id}`}
|
||||
usePortal
|
||||
toolbar={false}>
|
||||
{isCompatible && (
|
||||
<>
|
||||
{isEnabled ? (
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => disable(id)}
|
||||
>
|
||||
<Icon material="unpublished" />
|
||||
<span className="title" aria-disabled={isUninstalling}>Disable</span>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => enable(id)}
|
||||
>
|
||||
<Icon material="check_circle" />
|
||||
<span className="title" aria-disabled={isUninstalling}>Enable</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => uninstall(extension)}
|
||||
>
|
||||
<Icon material="delete" />
|
||||
<span className="title" aria-disabled={isUninstalling}>Uninstall</span>
|
||||
</MenuItem>
|
||||
</MenuActions>
|
||||
),
|
||||
};
|
||||
}), [extensions, extensionInstallationStateStore.anyUninstalling],
|
||||
);
|
||||
|
||||
const NonInjectedInstalledExtensions = observer(({
|
||||
extensionDiscovery,
|
||||
extensionInstallationStateStore,
|
||||
userExtensions,
|
||||
confirmUninstallExtension,
|
||||
enableExtension,
|
||||
disableExtension,
|
||||
}: Dependencies & InstalledExtensionsProps) => {
|
||||
if (!extensionDiscovery.isLoaded) {
|
||||
return <div><Spinner center /></div>;
|
||||
}
|
||||
|
||||
const extensions = userExtensions.get();
|
||||
|
||||
if (extensions.length == 0) {
|
||||
return (
|
||||
<div className="flex column h-full items-center justify-center">
|
||||
@@ -151,13 +73,96 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension
|
||||
);
|
||||
}
|
||||
|
||||
const toggleExtensionWith = (enabled: boolean) => (
|
||||
enabled
|
||||
? disableExtension
|
||||
: enableExtension
|
||||
);
|
||||
|
||||
return (
|
||||
<section data-testid="extensions-table">
|
||||
<List
|
||||
title={<h2 className={styles.title}>Installed extensions</h2>}
|
||||
columns={columns}
|
||||
data={data}
|
||||
items={extensions}
|
||||
columns={[
|
||||
{
|
||||
Header: "Name",
|
||||
accessor: "extension",
|
||||
width: 200,
|
||||
sortType: (rowA: Row, rowB: Row) => { // Custom sorting for extension name
|
||||
const nameA = extensions[rowA.index].manifest.name;
|
||||
const nameB = extensions[rowB.index].manifest.name;
|
||||
|
||||
if (nameA > nameB) return -1;
|
||||
if (nameB > nameA) return 1;
|
||||
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Version",
|
||||
accessor: "version",
|
||||
},
|
||||
{
|
||||
Header: "Status",
|
||||
accessor: "status",
|
||||
},
|
||||
{
|
||||
Header: "",
|
||||
accessor: "actions",
|
||||
disableSortBy: true,
|
||||
width: 20,
|
||||
},
|
||||
]}
|
||||
data={extensions.map(extension => {
|
||||
const { id, isEnabled, isCompatible, manifest } = extension;
|
||||
const { name, description, version } = manifest;
|
||||
const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id);
|
||||
const toggleExtension = toggleExtensionWith(isEnabled);
|
||||
|
||||
return {
|
||||
extension: (
|
||||
<div className={"flex items-start"}>
|
||||
<div>
|
||||
<div className={styles.extensionName}>{name}</div>
|
||||
<div className={styles.extensionDescription}>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
version,
|
||||
status: (
|
||||
<div className={cssNames({ [styles.enabled]: isEnabled, [styles.invalid]: !isCompatible })}>
|
||||
{getStatus(extension)}
|
||||
</div>
|
||||
),
|
||||
actions: (
|
||||
<MenuActions
|
||||
id={`menu-actions-for-installed-extensions-for-${id}`}
|
||||
usePortal
|
||||
toolbar={false}>
|
||||
{isCompatible && (
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => toggleExtension(id)}
|
||||
>
|
||||
<Icon material={isEnabled ? "unpublished" : "check_circle"} />
|
||||
<span className="title" aria-disabled={isUninstalling}>
|
||||
{isEnabled ? "Disable" : "Enabled"}
|
||||
</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
disabled={isUninstalling}
|
||||
onClick={() => confirmUninstallExtension(extension)}
|
||||
>
|
||||
<Icon material="delete" />
|
||||
<span className="title" aria-disabled={isUninstalling}>Uninstall</span>
|
||||
</MenuItem>
|
||||
</MenuActions>
|
||||
),
|
||||
};
|
||||
})}
|
||||
items={userExtensions.get()}
|
||||
filters={[
|
||||
(extension) => extension.manifest.name,
|
||||
(extension) => getStatus(extension),
|
||||
@@ -170,8 +175,12 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension
|
||||
|
||||
export const InstalledExtensions = withInjectables<Dependencies, InstalledExtensionsProps>(NonInjectedInstalledExtensions, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
...props,
|
||||
userExtensions: di.inject(userExtensionsInjectable),
|
||||
enableExtension: di.inject(enableExtensionInjectable),
|
||||
disableExtension: di.inject(disableExtensionInjectable),
|
||||
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
|
||||
}),
|
||||
});
|
||||
+2
-2
@@ -27,7 +27,7 @@ const uninstallExtensionInjectable = getInjectable({
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
|
||||
return async (extensionId: LensExtensionId): Promise<boolean> => {
|
||||
const ext = extensionLoader.getExtension(extensionId);
|
||||
const ext = extensionLoader.getExtensionById(extensionId);
|
||||
|
||||
if (!ext) {
|
||||
logger.debug(`[EXTENSIONS]: cannot uninstall ${extensionId}, was not installed`);
|
||||
@@ -45,7 +45,7 @@ const uninstallExtensionInjectable = getInjectable({
|
||||
await extensionDiscovery.uninstallExtension(extensionId);
|
||||
|
||||
// wait for the ExtensionLoader to actually uninstall the extension
|
||||
await when(() => !extensionLoader.userExtensions.has(extensionId));
|
||||
await when(() => !extensionLoader.userExtensions.get().has(extensionId));
|
||||
|
||||
showSuccessNotification(
|
||||
<p>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ const userExtensionsInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
|
||||
return computed(() => [...extensionLoader.userExtensions.values()]);
|
||||
return computed(() => [...extensionLoader.userExtensions.get().values()]);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const createExtensionInstanceInjectable = getInjectable({
|
||||
};
|
||||
|
||||
return (ExtensionClass, extension) => {
|
||||
const instance = new ExtensionClass(extension) as LensRendererExtension;
|
||||
const instance = new ExtensionClass(extension as any) as LensRendererExtension;
|
||||
|
||||
(instance as Writable<LensRendererExtension>)[lensExtensionDependencies] = deps;
|
||||
|
||||
|
||||
Reference in new issue
Block a user