mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Making apiBase injectable (#6022)
* Making apiBase injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all of Helm functions to be DI Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make PortForward's use of apiBase fully injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all metric requests to be injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace resource applier with injectables Signed-off-by: Sebastian Malton <sebastian@malton.name> * Switch KubeJsonApi.forCluster to be injectable but do not use Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert the rest of shell sessions to be DI-ed - This is a prerequesit for using the new createKubeJsonApiForClusterInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Use new createKubeJsonApiForClusterInjectable for openNodeShellSession Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make KubeconfigDialog injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove jest-fetch-mock and make fetch injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests with new global override Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add new injectable for create KubeJsonApi and JsonApi instances Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix showing-details-for-helm-release behavioural tests - Remove HelmChartStore in favour of all injectables - Create a model for UpgradeChartDockTab Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix show details and updating helm releases tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix residual typing issues related to metrics Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix crash on load due to circular dependency Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix create resource tab not working Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove legacy apiBase global Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce and use isDebuggingInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce and use windowLocationInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove global legacy apiKube Signed-off-by: Sebastian Malton <sebastian@malton.name> * Improve injectable filenames compared to the injectables inside Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove modifying input in requestActivePortForwardInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce and use get(Milli)SecondsFromUnixEpochInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Switch to non-reactive way of gettting possible helm release versions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix typo Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix bug in KubeApi constructor Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all KubeApi related tests to use asyncFn Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix unit tests after introducing new injectables that have side effects Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix bad rebase causing tests to fail Signed-off-by: Sebastian Malton <sebastian@malton.name> * Improve expects for multiple field values Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix crash will looking up api refs Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix breaking change on KubeApi.list Signed-off-by: Sebastian Malton <sebastian@malton.name> * Better fix for formatting urls Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove injectable for time since we should just use useMockTime Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add happy path behavioural tests for upgrade chart tab Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove debug message Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix showing-details-for-helm-release tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix installing-helm-chart-from-new-tab tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests relating to hosted cluster id Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots to recent changes in master Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Reupdated upgrade chart new tab test snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix flakiness in unit test when using <Animated> Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix flakiness and improve tests for DeleteClusterDialog Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix kubeconfig-sync tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix <Extensions> tests by removing mockFs and making everything injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix build issues Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix getElectronAppPathInjectable override not returning absolute paths - Also fixes the listing-active-helm-repos-in-prefs tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace all uses of getAbsolutePath with joinPaths as it is more correct and less confusing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix opening application window tests by making override properly absolute Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots relating no longer using getAbsolutePath Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix and add behavioural tests for RenderDelay Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix extension discovery tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix test flakiness because of path side effects, propagate uses to as many places Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix extension-discovery tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add global override to fix some tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Rewrite and fix implementation of KubeconfigManager and its tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests by global override pathExists Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix unit tests failing on windows by using injectable verions of path functions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Attempt to fix test timeout by using runInAction Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix setupIpcMainHandlers usage Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> Co-authored-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
367 files changed
+16701
-5302
No files matched your search
@@ -5,14 +5,11 @@
|
||||
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import fse from "fs-extra";
|
||||
import React from "react";
|
||||
import type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery";
|
||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
import { ConfirmDialog } from "../../confirm-dialog";
|
||||
import { Extensions } from "../extensions";
|
||||
import mockFs from "mock-fs";
|
||||
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import type { DiRender } from "../../test-utils/renderFor";
|
||||
@@ -20,18 +17,15 @@ import { renderFor } from "../../test-utils/renderFor";
|
||||
import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import directoryForDownloadsInjectable from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable";
|
||||
import getConfigurationFileModelInjectable from "../../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import assert from "assert";
|
||||
import type { InstallFromInput } from "../install-from-input/install-from-input";
|
||||
import installFromInputInjectable from "../install-from-input/install-from-input.injectable";
|
||||
import type { InstallExtensionFromInput } from "../install-extension-from-input.injectable";
|
||||
import installExtensionFromInputInjectable from "../install-extension-from-input.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import { observable, when } from "mobx";
|
||||
import type { DeleteFile } from "../../../../common/fs/delete-file.injectable";
|
||||
import deleteFileInjectable from "../../../../common/fs/delete-file.injectable";
|
||||
|
||||
mockWindow();
|
||||
|
||||
jest.setTimeout(30000);
|
||||
jest.mock("fs-extra");
|
||||
jest.mock("../../notifications");
|
||||
|
||||
jest.mock("../../../../common/utils/downloadFile", () => ({
|
||||
@@ -52,9 +46,10 @@ jest.mock("../../../../common/utils/tar");
|
||||
describe("Extensions", () => {
|
||||
let extensionLoader: ExtensionLoader;
|
||||
let extensionDiscovery: ExtensionDiscovery;
|
||||
let installFromInput: jest.MockedFunction<InstallFromInput>;
|
||||
let installExtensionFromInput: jest.MockedFunction<InstallExtensionFromInput>;
|
||||
let extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
let render: DiRender;
|
||||
let deleteFileMock: jest.MockedFunction<DeleteFile>;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
@@ -62,17 +57,13 @@ describe("Extensions", () => {
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
di.override(directoryForDownloadsInjectable, () => "some-directory-for-downloads");
|
||||
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
|
||||
mockFs({
|
||||
"some-directory-for-user-data": {},
|
||||
});
|
||||
|
||||
render = renderFor(di);
|
||||
|
||||
installFromInput = jest.fn();
|
||||
installExtensionFromInput = jest.fn();
|
||||
di.override(installExtensionFromInputInjectable, () => installExtensionFromInput);
|
||||
|
||||
di.override(installFromInputInjectable, () => installFromInput);
|
||||
deleteFileMock = jest.fn();
|
||||
di.override(deleteFileInjectable, () => deleteFileMock);
|
||||
|
||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
@@ -95,10 +86,6 @@ describe("Extensions", () => {
|
||||
extensionDiscovery.uninstallExtension = jest.fn(() => Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("disables uninstall and disable buttons while uninstalling", async () => {
|
||||
extensionDiscovery.isLoaded = true;
|
||||
|
||||
@@ -138,8 +125,8 @@ describe("Extensions", () => {
|
||||
|
||||
const resolveInstall = observable.box(false);
|
||||
|
||||
(fse.unlink as jest.MockedFunction<typeof fse.unlink>).mockReturnValue(Promise.resolve());
|
||||
installFromInput.mockImplementation(async (input) => {
|
||||
deleteFileMock.mockReturnValue(Promise.resolve());
|
||||
installExtensionFromInput.mockImplementation(async (input) => {
|
||||
expect(input).toBe("https://test.extensionurl/package.tgz");
|
||||
|
||||
const clear = extensionInstallationStateStore.startPreInstall();
|
||||
|
||||
@@ -2,22 +2,18 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { ExtendableDisposer } from "../../../common/utils";
|
||||
import { downloadFile, downloadJson } from "../../../common/utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import React from "react";
|
||||
import path from "path";
|
||||
import { SemVer } from "semver";
|
||||
import URLParse from "url-parse";
|
||||
import type { InstallRequest } from "./attempt-install/install-request";
|
||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import type { Confirm } from "../confirm-dialog/confirm.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import attemptInstallInjectable from "./attempt-install/attempt-install.injectable";
|
||||
import getBaseRegistryUrlInjectable from "./get-base-registry-url/get-base-registry-url.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import confirmInjectable from "../confirm-dialog/confirm.injectable";
|
||||
import { reduce } from "lodash";
|
||||
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
||||
|
||||
export interface ExtensionInfo {
|
||||
name: string;
|
||||
@@ -27,126 +23,113 @@ export interface ExtensionInfo {
|
||||
|
||||
export type AttemptInstallByInfo = (info: ExtensionInfo) => Promise<void>;
|
||||
|
||||
interface Dependencies {
|
||||
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>;
|
||||
getBaseRegistryUrl: () => Promise<string>;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
confirm: Confirm;
|
||||
}
|
||||
const attemptInstallByInfoInjectable = getInjectable({
|
||||
id: "attempt-install-by-info",
|
||||
instantiate: (di): AttemptInstallByInfo => {
|
||||
const attemptInstall = di.inject(attemptInstallInjectable);
|
||||
const getBaseRegistryUrl = di.inject(getBaseRegistryUrlInjectable);
|
||||
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||
const confirm = di.inject(confirmInjectable);
|
||||
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
|
||||
|
||||
const attemptInstallByInfo = ({
|
||||
attemptInstall,
|
||||
getBaseRegistryUrl,
|
||||
extensionInstallationStateStore,
|
||||
confirm,
|
||||
}: Dependencies): AttemptInstallByInfo => (
|
||||
async (info) => {
|
||||
const { name, version, requireConfirmation = false } = info;
|
||||
const disposer = extensionInstallationStateStore.startPreInstall();
|
||||
const baseUrl = await getBaseRegistryUrl();
|
||||
const registryUrl = new URLParse(baseUrl).set("pathname", name).toString();
|
||||
let json: any;
|
||||
let finalVersion = version;
|
||||
return async (info) => {
|
||||
const { name, version, requireConfirmation = false } = info;
|
||||
const disposer = extensionInstallationStateStore.startPreInstall();
|
||||
const baseUrl = await getBaseRegistryUrl();
|
||||
const registryUrl = new URLParse(baseUrl).set("pathname", name).toString();
|
||||
let json: any;
|
||||
let finalVersion = version;
|
||||
|
||||
try {
|
||||
json = await downloadJson({ url: registryUrl }).promise;
|
||||
try {
|
||||
json = await downloadJson({ url: registryUrl }).promise;
|
||||
|
||||
if (!json || json.error || typeof json.versions !== "object" || !json.versions) {
|
||||
const message = json?.error ? `: ${json.error}` : "";
|
||||
if (!json || json.error || typeof json.versions !== "object" || !json.versions) {
|
||||
const message = json?.error ? `: ${json.error}` : "";
|
||||
|
||||
Notifications.error(`Failed to get registry information for that extension${message}`);
|
||||
|
||||
return disposer();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
// assume invalid JSON
|
||||
console.warn("Set registry has invalid json", { url: baseUrl }, error);
|
||||
Notifications.error("Failed to get valid registry information for that extension. Registry did not return valid JSON");
|
||||
} else {
|
||||
console.error("Failed to download registry information", error);
|
||||
Notifications.error(`Failed to get valid registry information for that extension. ${error}`);
|
||||
}
|
||||
|
||||
return disposer();
|
||||
}
|
||||
|
||||
if (version) {
|
||||
if (!json.versions[version]) {
|
||||
if (json["dist-tags"][version]) {
|
||||
finalVersion = json["dist-tags"][version];
|
||||
} else {
|
||||
Notifications.error((
|
||||
<p>
|
||||
{"The "}
|
||||
<em>{name}</em>
|
||||
{" extension does not have a version or tag "}
|
||||
<code>{version}</code>
|
||||
.
|
||||
</p>
|
||||
));
|
||||
Notifications.error(`Failed to get registry information for that extension${message}`);
|
||||
|
||||
return disposer();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
// assume invalid JSON
|
||||
console.warn("Set registry has invalid json", { url: baseUrl }, error);
|
||||
Notifications.error("Failed to get valid registry information for that extension. Registry did not return valid JSON");
|
||||
} else {
|
||||
console.error("Failed to download registry information", error);
|
||||
Notifications.error(`Failed to get valid registry information for that extension. ${error}`);
|
||||
}
|
||||
|
||||
return disposer();
|
||||
}
|
||||
} else {
|
||||
const versions = Object.keys(json.versions)
|
||||
.map(version => new SemVer(version, { loose: true }))
|
||||
|
||||
if (version) {
|
||||
if (!json.versions[version]) {
|
||||
if (json["dist-tags"][version]) {
|
||||
finalVersion = json["dist-tags"][version];
|
||||
} else {
|
||||
Notifications.error((
|
||||
<p>
|
||||
{"The "}
|
||||
<em>{name}</em>
|
||||
{" extension does not have a version or tag "}
|
||||
<code>{version}</code>
|
||||
.
|
||||
</p>
|
||||
));
|
||||
|
||||
return disposer();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const versions = Object.keys(json.versions)
|
||||
.map(version => new SemVer(version, { loose: true }))
|
||||
// ignore pre-releases for auto picking the version
|
||||
.filter(version => version.prerelease.length === 0);
|
||||
.filter(version => version.prerelease.length === 0);
|
||||
|
||||
const latestVersion = reduce(versions, (prev, curr) => prev.compareMain(curr) === -1 ? curr : prev);
|
||||
const latestVersion = reduce(versions, (prev, curr) => prev.compareMain(curr) === -1 ? curr : prev);
|
||||
|
||||
if (!latestVersion) {
|
||||
console.error("No versions supplied for that extension", { name });
|
||||
Notifications.error(`No versions found for ${name}`);
|
||||
if (!latestVersion) {
|
||||
console.error("No versions supplied for that extension", { name });
|
||||
Notifications.error(`No versions found for ${name}`);
|
||||
|
||||
return disposer();
|
||||
return disposer();
|
||||
}
|
||||
|
||||
finalVersion = latestVersion.format();
|
||||
}
|
||||
|
||||
finalVersion = latestVersion.format();
|
||||
}
|
||||
if (requireConfirmation) {
|
||||
const proceed = await confirm({
|
||||
message: (
|
||||
<p>
|
||||
Are you sure you want to install
|
||||
{" "}
|
||||
<b>
|
||||
{name}
|
||||
@
|
||||
{finalVersion}
|
||||
</b>
|
||||
?
|
||||
</p>
|
||||
),
|
||||
labelCancel: "Cancel",
|
||||
labelOk: "Install",
|
||||
});
|
||||
|
||||
if (requireConfirmation) {
|
||||
const proceed = await confirm({
|
||||
message: (
|
||||
<p>
|
||||
Are you sure you want to install
|
||||
{" "}
|
||||
<b>
|
||||
{name}
|
||||
@
|
||||
{finalVersion}
|
||||
</b>
|
||||
?
|
||||
</p>
|
||||
),
|
||||
labelCancel: "Cancel",
|
||||
labelOk: "Install",
|
||||
});
|
||||
|
||||
if (!proceed) {
|
||||
return disposer();
|
||||
if (!proceed) {
|
||||
return disposer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const url = json.versions[finalVersion!].dist.tarball;
|
||||
const fileName = path.basename(url);
|
||||
const { promise: dataP } = downloadFile({ url, timeout: 10 * 60 * 1000 });
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const url = json.versions[finalVersion!].dist.tarball;
|
||||
const fileName = getBasenameOfPath(url);
|
||||
const { promise: dataP } = downloadFile({ url, timeout: 10 * 60 * 1000 });
|
||||
|
||||
return attemptInstall({ fileName, dataP }, disposer);
|
||||
}
|
||||
);
|
||||
|
||||
const attemptInstallByInfoInjectable = getInjectable({
|
||||
id: "attempt-install-by-info",
|
||||
instantiate: (di) => attemptInstallByInfo({
|
||||
attemptInstall: di.inject(attemptInstallInjectable),
|
||||
getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
confirm: di.inject(confirmInjectable),
|
||||
}),
|
||||
return attemptInstall({ fileName, dataP }, disposer);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default attemptInstallByInfoInjectable;
|
||||
@@ -2,12 +2,8 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type {
|
||||
Disposer,
|
||||
ExtendableDisposer } from "../../../../common/utils";
|
||||
import {
|
||||
disposer,
|
||||
} from "../../../../common/utils";
|
||||
import type { ExtendableDisposer } from "../../../../common/utils";
|
||||
import { disposer } from "../../../../common/utils";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { Button } from "../../button";
|
||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
@@ -15,29 +11,19 @@ import type { LensExtensionId } from "../../../../extensions/lens-extension";
|
||||
import React from "react";
|
||||
import { remove as removeDir } from "fs-extra";
|
||||
import { shell } from "electron";
|
||||
import type { InstallRequestValidated } from "./create-temp-files-and-validate/create-temp-files-and-validate";
|
||||
import type { InstallRequest } from "./install-request";
|
||||
import type {
|
||||
ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import {
|
||||
ExtensionInstallationState,
|
||||
} from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import { ExtensionInstallationState } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import type { UnpackExtension } from "./unpack-extension/unpack-extension.injectable";
|
||||
import type { CreateTempFilesAndValidate } from "./create-temp-files-and-validate/create-temp-files-and-validate.injectable";
|
||||
import type { GetExtensionDestFolder } from "./get-extension-dest-folder/get-extension-dest-folder.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
uninstallExtension: (id: LensExtensionId) => Promise<boolean>;
|
||||
|
||||
unpackExtension: (
|
||||
request: InstallRequestValidated,
|
||||
disposeDownloading: Disposer,
|
||||
) => Promise<void>;
|
||||
|
||||
createTempFilesAndValidate: (
|
||||
installRequest: InstallRequest,
|
||||
) => Promise<InstallRequestValidated | null>;
|
||||
|
||||
getExtensionDestFolder: (name: string) => string;
|
||||
|
||||
unpackExtension: UnpackExtension;
|
||||
createTempFilesAndValidate: CreateTempFilesAndValidate;
|
||||
getExtensionDestFolder: GetExtensionDestFolder;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
@@ -51,6 +37,8 @@ export const attemptInstall =
|
||||
extensionInstallationStateStore,
|
||||
}: Dependencies) =>
|
||||
async (request: InstallRequest, d?: ExtendableDisposer): Promise<void> => {
|
||||
console.log("Attempting to install extension");
|
||||
|
||||
const dispose = disposer(
|
||||
extensionInstallationStateStore.startPreInstall(),
|
||||
d,
|
||||
|
||||
+91
-5
@@ -3,16 +3,102 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { createTempFilesAndValidate } from "./create-temp-files-and-validate";
|
||||
import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import React from "react";
|
||||
import type { LensExtensionId, LensExtensionManifest } from "../../../../../extensions/lens-extension";
|
||||
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
||||
import type { InstallRequest } from "../install-request";
|
||||
import { validatePackage } from "../validate-package/validate-package";
|
||||
import joinPathsInjectable from "../../../../../common/path/join-paths.injectable";
|
||||
import tempDirectoryPathInjectable from "../../../../../common/os/temp-directory-path.injectable";
|
||||
import ensureDirInjectable from "../../../../../common/fs/ensure-dir.injectable";
|
||||
import writeFileInjectable from "../../../../../common/fs/write-file.injectable";
|
||||
import loggerInjectable from "../../../../../common/logger.injectable";
|
||||
import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable";
|
||||
|
||||
export interface InstallRequestValidated {
|
||||
fileName: string;
|
||||
data: Buffer;
|
||||
id: LensExtensionId;
|
||||
manifest: LensExtensionManifest;
|
||||
tempFile: string; // temp system path to packed extension for unpacking
|
||||
}
|
||||
|
||||
export type CreateTempFilesAndValidate = (req: InstallRequest) => Promise<InstallRequestValidated | null>;
|
||||
|
||||
const createTempFilesAndValidateInjectable = getInjectable({
|
||||
id: "create-temp-files-and-validate",
|
||||
|
||||
instantiate: (di) =>
|
||||
createTempFilesAndValidate({
|
||||
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||
}),
|
||||
instantiate: (di) => {
|
||||
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const tempDirectoryPath = di.inject(tempDirectoryPathInjectable);
|
||||
const ensureDir = di.inject(ensureDirInjectable);
|
||||
const writeFile = di.inject(writeFileInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
|
||||
const baseTempExtensionsDirectory = joinPaths(tempDirectoryPath, "lens-extensions");
|
||||
const getExtensionPackageTemp = (fileName: string) => joinPaths(baseTempExtensionsDirectory, fileName);
|
||||
|
||||
return async ({
|
||||
fileName,
|
||||
dataP,
|
||||
}: InstallRequest): Promise<InstallRequestValidated | null> => {
|
||||
// copy files to temp
|
||||
await ensureDir(baseTempExtensionsDirectory);
|
||||
|
||||
// validate packages
|
||||
const tempFile = getExtensionPackageTemp(fileName);
|
||||
|
||||
try {
|
||||
const data = await dataP;
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await writeFile(tempFile, data);
|
||||
logger.info("validating package", tempFile);
|
||||
const manifest = await validatePackage(tempFile);
|
||||
const id = joinPaths(
|
||||
extensionDiscovery.nodeModulesPath,
|
||||
manifest.name,
|
||||
"package.json",
|
||||
);
|
||||
|
||||
return {
|
||||
fileName,
|
||||
data,
|
||||
manifest,
|
||||
tempFile,
|
||||
id,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(
|
||||
`[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`,
|
||||
{ error },
|
||||
);
|
||||
showErrorNotification(
|
||||
<div className="flex column gaps">
|
||||
<p>
|
||||
{"Installing "}
|
||||
<em>{fileName}</em>
|
||||
{" has failed, skipping."}
|
||||
</p>
|
||||
<p>
|
||||
{"Reason: "}
|
||||
<em>{message}</em>
|
||||
</p>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default createTempFilesAndValidateInjectable;
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { validatePackage } from "../validate-package/validate-package";
|
||||
import type { ExtensionDiscovery } from "../../../../../extensions/extension-discovery/extension-discovery";
|
||||
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
||||
import logger from "../../../../../main/logger";
|
||||
import { Notifications } from "../../../notifications";
|
||||
import path from "path";
|
||||
import fse from "fs-extra";
|
||||
import React from "react";
|
||||
import os from "os";
|
||||
import type {
|
||||
LensExtensionId,
|
||||
LensExtensionManifest,
|
||||
} from "../../../../../extensions/lens-extension";
|
||||
import type { InstallRequest } from "../install-request";
|
||||
|
||||
export interface InstallRequestValidated {
|
||||
fileName: string;
|
||||
data: Buffer;
|
||||
id: LensExtensionId;
|
||||
manifest: LensExtensionManifest;
|
||||
tempFile: string; // temp system path to packed extension for unpacking
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
extensionDiscovery: ExtensionDiscovery;
|
||||
}
|
||||
|
||||
export const createTempFilesAndValidate =
|
||||
({ extensionDiscovery }: Dependencies) =>
|
||||
async ({
|
||||
fileName,
|
||||
dataP,
|
||||
}: InstallRequest): Promise<InstallRequestValidated | null> => {
|
||||
// copy files to temp
|
||||
await fse.ensureDir(getExtensionPackageTemp());
|
||||
|
||||
// validate packages
|
||||
const tempFile = getExtensionPackageTemp(fileName);
|
||||
|
||||
try {
|
||||
const data = await dataP;
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await fse.writeFile(tempFile, data);
|
||||
const manifest = await validatePackage(tempFile);
|
||||
const id = path.join(
|
||||
extensionDiscovery.nodeModulesPath,
|
||||
manifest.name,
|
||||
"package.json",
|
||||
);
|
||||
|
||||
return {
|
||||
fileName,
|
||||
data,
|
||||
manifest,
|
||||
tempFile,
|
||||
id,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(
|
||||
`[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`,
|
||||
{ error },
|
||||
);
|
||||
Notifications.error(
|
||||
<div className="flex column gaps">
|
||||
<p>
|
||||
{"Installing "}
|
||||
<em>{fileName}</em>
|
||||
{" has failed, skipping."}
|
||||
</p>
|
||||
<p>
|
||||
{"Reason: "}
|
||||
<em>{message}</em>
|
||||
</p>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
function getExtensionPackageTemp(fileName = "") {
|
||||
return path.join(os.tmpdir(), "lens-extensions", fileName);
|
||||
}
|
||||
+9
-6
@@ -3,18 +3,21 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
|
||||
import joinPathsInjectable from "../../../../../common/path/join-paths.injectable";
|
||||
import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import { sanitizeExtensionName } from "../../../../../extensions/lens-extension";
|
||||
|
||||
import { getExtensionDestFolder } from "./get-extension-dest-folder";
|
||||
export type GetExtensionDestFolder = (extensionName: string) => string;
|
||||
|
||||
const getExtensionDestFolderInjectable = getInjectable({
|
||||
id: "get-extension-dest-folder",
|
||||
|
||||
instantiate: (di) =>
|
||||
getExtensionDestFolder({
|
||||
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||
}),
|
||||
instantiate: (di): GetExtensionDestFolder => {
|
||||
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
|
||||
return (name) => joinPaths(extensionDiscovery.localFolderPath, sanitizeExtensionName(name));
|
||||
},
|
||||
});
|
||||
|
||||
export default getExtensionDestFolderInjectable;
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { ExtensionDiscovery } from "../../../../../extensions/extension-discovery/extension-discovery";
|
||||
import { sanitizeExtensionName } from "../../../../../extensions/lens-extension";
|
||||
import path from "path";
|
||||
|
||||
interface Dependencies {
|
||||
extensionDiscovery: ExtensionDiscovery;
|
||||
}
|
||||
|
||||
export const getExtensionDestFolder =
|
||||
({ extensionDiscovery }: Dependencies) =>
|
||||
(name: string) =>
|
||||
path.join(extensionDiscovery.localFolderPath, sanitizeExtensionName(name));
|
||||
+117
-11
@@ -2,23 +2,129 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { unpackExtension } from "./unpack-extension";
|
||||
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import getExtensionDestFolderInjectable
|
||||
from "../get-extension-dest-folder/get-extension-dest-folder.injectable";
|
||||
import extensionInstallationStateStoreInjectable
|
||||
from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import getExtensionDestFolderInjectable from "../get-extension-dest-folder/get-extension-dest-folder.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate.injectable";
|
||||
import type { Disposer } from "../../../../utils";
|
||||
import { noop } from "../../../../utils";
|
||||
import { extensionDisplayName } from "../../../../../extensions/lens-extension";
|
||||
import joinPathsInjectable from "../../../../../common/path/join-paths.injectable";
|
||||
import loggerInjectable from "../../../../../common/logger.injectable";
|
||||
import { when } from "mobx";
|
||||
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
||||
import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable";
|
||||
import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable";
|
||||
import getDirnameOfPathInjectable from "../../../../../common/path/get-dirname.injectable";
|
||||
import getBasenameOfPathInjectable from "../../../../../common/path/get-basename.injectable";
|
||||
import extractTarInjectable from "../../../../../common/fs/extract-tar.injectable";
|
||||
import ensureDirInjectable from "../../../../../common/fs/ensure-dir.injectable";
|
||||
import removePathInjectable from "../../../../../common/fs/remove-path.injectable";
|
||||
import deleteFileInjectable from "../../../../../common/fs/delete-file.injectable";
|
||||
import readDirectoryInjectable from "../../../../../common/fs/read-directory.injectable";
|
||||
import moveInjectable from "../../../../../common/fs/move.injectable";
|
||||
|
||||
export type UnpackExtension = (request: InstallRequestValidated, disposeDownloading?: Disposer) => Promise<void>;
|
||||
|
||||
const unpackExtensionInjectable = getInjectable({
|
||||
id: "unpack-extension",
|
||||
|
||||
instantiate: (di) =>
|
||||
unpackExtension({
|
||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||
getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
}),
|
||||
instantiate: (di): UnpackExtension => {
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
const getExtensionDestFolder = di.inject(getExtensionDestFolderInjectable);
|
||||
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
|
||||
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const showOkNotification = di.inject(showSuccessNotificationInjectable);
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
const extractTar = di.inject(extractTarInjectable);
|
||||
const ensureDir = di.inject(ensureDirInjectable);
|
||||
const removePath = di.inject(removePathInjectable);
|
||||
const deleteFile = di.inject(deleteFileInjectable);
|
||||
const readDirectory = di.inject(readDirectoryInjectable);
|
||||
const move = di.inject(moveInjectable);
|
||||
|
||||
return async (request, disposeDownloading) => {
|
||||
const {
|
||||
id,
|
||||
fileName,
|
||||
tempFile,
|
||||
manifest: { name, version },
|
||||
} = request;
|
||||
|
||||
extensionInstallationStateStore.setInstalling(id);
|
||||
disposeDownloading?.();
|
||||
|
||||
const displayName = extensionDisplayName(name, version);
|
||||
const extensionFolder = getExtensionDestFolder(name);
|
||||
const unpackingTempFolder = joinPaths(
|
||||
getDirnameOfPath(tempFile),
|
||||
`${getBasenameOfPath(tempFile)}-unpacked`,
|
||||
);
|
||||
|
||||
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
||||
|
||||
try {
|
||||
// extract to temp folder first
|
||||
await removePath(unpackingTempFolder).catch(noop);
|
||||
await ensureDir(unpackingTempFolder);
|
||||
await extractTar(tempFile, { cwd: unpackingTempFolder });
|
||||
|
||||
// move contents to extensions folder
|
||||
const unpackedFiles = await readDirectory(unpackingTempFolder);
|
||||
let unpackedRootFolder = unpackingTempFolder;
|
||||
|
||||
if (unpackedFiles.length === 1) {
|
||||
// check if %extension.tgz was packed with single top folder,
|
||||
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
|
||||
unpackedRootFolder = joinPaths(unpackingTempFolder, unpackedFiles[0]);
|
||||
}
|
||||
|
||||
await ensureDir(extensionFolder);
|
||||
await move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
||||
|
||||
// wait for the loader has actually install it
|
||||
await when(() => extensionLoader.userExtensions.has(id));
|
||||
|
||||
// Enable installed extensions by default.
|
||||
extensionLoader.setIsEnabled(id, true);
|
||||
|
||||
showOkNotification(
|
||||
<p>
|
||||
{"Extension "}
|
||||
<b>{displayName}</b>
|
||||
{" successfully installed!"}
|
||||
</p>,
|
||||
);
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(
|
||||
`[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`,
|
||||
{ error },
|
||||
);
|
||||
showErrorNotification(
|
||||
<p>
|
||||
{"Installing extension "}
|
||||
<b>{displayName}</b>
|
||||
{" has failed: "}
|
||||
<em>{message}</em>
|
||||
</p>,
|
||||
);
|
||||
} finally {
|
||||
// Remove install state once finished
|
||||
extensionInstallationStateStore.clearInstalling(id);
|
||||
|
||||
// clean up
|
||||
removePath(unpackingTempFolder).catch(noop);
|
||||
deleteFile(tempFile).catch(noop);
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default unpackExtensionInjectable;
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate";
|
||||
import type { Disposer } from "../../../../../common/utils";
|
||||
import { extractTar, noop } from "../../../../../common/utils";
|
||||
import { extensionDisplayName } from "../../../../../extensions/lens-extension";
|
||||
import logger from "../../../../../main/logger";
|
||||
import type { ExtensionLoader } from "../../../../../extensions/extension-loader";
|
||||
import { Notifications } from "../../../notifications";
|
||||
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
||||
import path from "path";
|
||||
import fse from "fs-extra";
|
||||
import { when } from "mobx";
|
||||
import React from "react";
|
||||
import type { ExtensionInstallationStateStore } from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
getExtensionDestFolder: (name: string) => string;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
export const unpackExtension =
|
||||
({
|
||||
extensionLoader,
|
||||
getExtensionDestFolder,
|
||||
extensionInstallationStateStore,
|
||||
}: Dependencies) =>
|
||||
async (request: InstallRequestValidated, disposeDownloading?: Disposer) => {
|
||||
const {
|
||||
id,
|
||||
fileName,
|
||||
tempFile,
|
||||
manifest: { name, version },
|
||||
} = request;
|
||||
|
||||
extensionInstallationStateStore.setInstalling(id);
|
||||
disposeDownloading?.();
|
||||
|
||||
const displayName = extensionDisplayName(name, version);
|
||||
const extensionFolder = getExtensionDestFolder(name);
|
||||
const unpackingTempFolder = path.join(
|
||||
path.dirname(tempFile),
|
||||
`${path.basename(tempFile)}-unpacked`,
|
||||
);
|
||||
|
||||
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
||||
|
||||
try {
|
||||
// extract to temp folder first
|
||||
await fse.remove(unpackingTempFolder).catch(noop);
|
||||
await fse.ensureDir(unpackingTempFolder);
|
||||
await extractTar(tempFile, { cwd: unpackingTempFolder });
|
||||
|
||||
// move contents to extensions folder
|
||||
const unpackedFiles = await fse.readdir(unpackingTempFolder);
|
||||
let unpackedRootFolder = unpackingTempFolder;
|
||||
|
||||
if (unpackedFiles.length === 1) {
|
||||
// check if %extension.tgz was packed with single top folder,
|
||||
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
|
||||
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);
|
||||
}
|
||||
|
||||
await fse.ensureDir(extensionFolder);
|
||||
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
||||
|
||||
// wait for the loader has actually install it
|
||||
await when(() => extensionLoader.userExtensions.has(id));
|
||||
|
||||
// Enable installed extensions by default.
|
||||
extensionLoader.setIsEnabled(id, true);
|
||||
|
||||
Notifications.ok(
|
||||
<p>
|
||||
{"Extension "}
|
||||
<b>{displayName}</b>
|
||||
{" successfully installed!"}
|
||||
</p>,
|
||||
);
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(
|
||||
`[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`,
|
||||
{ error },
|
||||
);
|
||||
Notifications.error(
|
||||
<p>
|
||||
{"Installing extension "}
|
||||
<b>{displayName}</b>
|
||||
{" has failed: "}
|
||||
<em>{message}</em>
|
||||
</p>,
|
||||
);
|
||||
} finally {
|
||||
// Remove install state once finished
|
||||
extensionInstallationStateStore.clearInstalling(id);
|
||||
|
||||
// clean up
|
||||
fse.remove(unpackingTempFolder).catch(noop);
|
||||
fse.unlink(tempFile).catch(noop);
|
||||
}
|
||||
};
|
||||
+18
-5
@@ -3,16 +3,29 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { attemptInstalls } from "./attempt-installs";
|
||||
import getBasenameOfPathInjectable from "../../../../common/path/get-basename.injectable";
|
||||
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
|
||||
import readFileNotifyInjectable from "../read-file-notify/read-file-notify.injectable";
|
||||
|
||||
export type AttemptInstalls = (filePaths: string[]) => Promise<void>;
|
||||
|
||||
const attemptInstallsInjectable = getInjectable({
|
||||
id: "attempt-installs",
|
||||
|
||||
instantiate: (di) =>
|
||||
attemptInstalls({
|
||||
attemptInstall: di.inject(attemptInstallInjectable),
|
||||
}),
|
||||
instantiate: (di): AttemptInstalls => {
|
||||
const attemptInstall = di.inject(attemptInstallInjectable);
|
||||
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
|
||||
const readFileNotify = di.inject(readFileNotifyInjectable);
|
||||
|
||||
return async (filePaths) => {
|
||||
await Promise.allSettled(filePaths.map(filePath => (
|
||||
attemptInstall({
|
||||
fileName: getBasenameOfPath(filePath),
|
||||
dataP: readFileNotify(filePath),
|
||||
})
|
||||
)));
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default attemptInstallsInjectable;
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { readFileNotify } from "../read-file-notify/read-file-notify";
|
||||
import path from "path";
|
||||
import type { InstallRequest } from "../attempt-install/install-request";
|
||||
|
||||
interface Dependencies {
|
||||
attemptInstall: (request: InstallRequest) => Promise<void>;
|
||||
}
|
||||
|
||||
export const attemptInstalls =
|
||||
({ attemptInstall }: Dependencies) =>
|
||||
async (filePaths: string[]): Promise<void> => {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
promises.push(
|
||||
attemptInstall({
|
||||
fileName: path.basename(filePath),
|
||||
dataP: readFileNotify(filePath),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
};
|
||||
@@ -28,21 +28,21 @@ import enableExtensionInjectable from "./enable-extension/enable-extension.injec
|
||||
import disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
|
||||
import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable";
|
||||
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable";
|
||||
import installFromInputInjectable from "./install-from-input/install-from-input.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 { LensExtensionId } from "../../../extensions/lens-extension";
|
||||
import installOnDropInjectable from "./install-on-drop/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 type { InstallFromInput } from "./install-from-input/install-from-input";
|
||||
|
||||
interface Dependencies {
|
||||
userExtensions: IComputedValue<InstalledExtension[]>;
|
||||
enableExtension: (id: LensExtensionId) => void;
|
||||
disableExtension: (id: LensExtensionId) => void;
|
||||
confirmUninstallExtension: ConfirmUninstallExtension;
|
||||
installFromInput: InstallFromInput;
|
||||
installExtensionFromInput: InstallExtensionFromInput;
|
||||
installFromSelectFileDialog: () => Promise<void>;
|
||||
installOnDrop: (files: File[]) => Promise<void>;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
@@ -107,7 +107,7 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
|
||||
<Install
|
||||
supportedFormats={supportedExtensionFormats}
|
||||
onChange={value => (this.installPath = value)}
|
||||
installFromInput={() => this.props.installFromInput(this.installPath)}
|
||||
installFromInput={() => this.props.installExtensionFromInput(this.installPath)}
|
||||
installFromSelectFileDialog={this.props.installFromSelectFileDialog}
|
||||
installPath={this.installPath}
|
||||
/>
|
||||
@@ -131,7 +131,7 @@ export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
|
||||
enableExtension: di.inject(enableExtensionInjectable),
|
||||
disableExtension: di.inject(disableExtensionInjectable),
|
||||
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
|
||||
installFromInput: di.inject(installFromInputInjectable),
|
||||
installExtensionFromInput: di.inject(installExtensionFromInputInjectable),
|
||||
installOnDrop: di.inject(installOnDropInjectable),
|
||||
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
import type { ExtendableDisposer } from "../../../common/utils";
|
||||
import { downloadFile } from "../../../common/utils";
|
||||
import { InputValidators } from "../input";
|
||||
import { getMessageFromError } from "./get-message-from-error/get-message-from-error";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import attemptInstallInjectable from "./attempt-install/attempt-install.injectable";
|
||||
import attemptInstallByInfoInjectable from "./attempt-install-by-info.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import readFileNotifyInjectable from "./read-file-notify/read-file-notify.injectable";
|
||||
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
||||
import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
|
||||
export type InstallExtensionFromInput = (input: string) => Promise<void>;
|
||||
|
||||
const installExtensionFromInputInjectable = getInjectable({
|
||||
id: "install-extension-from-input",
|
||||
|
||||
instantiate: (di): InstallExtensionFromInput => {
|
||||
const attemptInstall = di.inject(attemptInstallInjectable);
|
||||
const attemptInstallByInfo = di.inject(attemptInstallByInfoInjectable);
|
||||
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||
const readFileNotify = di.inject(readFileNotifyInjectable);
|
||||
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
return async (input) => {
|
||||
let disposer: ExtendableDisposer | undefined = undefined;
|
||||
|
||||
try {
|
||||
// fixme: improve error messages for non-tar-file URLs
|
||||
if (InputValidators.isUrl.validate(input)) {
|
||||
// install via url
|
||||
disposer = extensionInstallationStateStore.startPreInstall();
|
||||
const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 });
|
||||
const fileName = getBasenameOfPath(input);
|
||||
|
||||
return await attemptInstall({ fileName, dataP: promise }, disposer);
|
||||
}
|
||||
|
||||
try {
|
||||
await InputValidators.isPath.validate(input);
|
||||
|
||||
// install from system path
|
||||
const fileName = getBasenameOfPath(input);
|
||||
|
||||
return await attemptInstall({ fileName, dataP: readFileNotify(input) });
|
||||
} catch (error) {
|
||||
const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input);
|
||||
|
||||
if (extNameCaptures) {
|
||||
const { name, version } = extNameCaptures;
|
||||
|
||||
return await attemptInstallByInfo({ name, version });
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unknown format of input: ${input}`);
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(`[EXTENSION-INSTALL]: installation has failed: ${message}`, { error, installPath: input });
|
||||
showErrorNotification((
|
||||
<p>
|
||||
{"Installation has failed: "}
|
||||
<b>{message}</b>
|
||||
</p>
|
||||
));
|
||||
} finally {
|
||||
disposer?.();
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default installExtensionFromInputInjectable;
|
||||
-23
@@ -1,23 +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 attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
|
||||
import { installFromInput } from "./install-from-input";
|
||||
import attemptInstallByInfoInjectable from "../attempt-install-by-info.injectable";
|
||||
import extensionInstallationStateStoreInjectable
|
||||
from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
|
||||
const installFromInputInjectable = getInjectable({
|
||||
id: "install-extension-from-input",
|
||||
|
||||
instantiate: (di) =>
|
||||
installFromInput({
|
||||
attemptInstall: di.inject(attemptInstallInjectable),
|
||||
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default installFromInputInjectable;
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { ExtendableDisposer } from "../../../../common/utils";
|
||||
import { downloadFile } from "../../../../common/utils";
|
||||
import { InputValidators } from "../../input";
|
||||
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
||||
import logger from "../../../../main/logger";
|
||||
import { Notifications } from "../../notifications";
|
||||
import path from "path";
|
||||
import React from "react";
|
||||
import { readFileNotify } from "../read-file-notify/read-file-notify";
|
||||
import type { InstallRequest } from "../attempt-install/install-request";
|
||||
import type { ExtensionInfo } from "../attempt-install-by-info.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
|
||||
export type InstallFromInput = (input: string) => Promise<void>;
|
||||
|
||||
interface Dependencies {
|
||||
attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => Promise<void>;
|
||||
attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise<void>;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
export const installFromInput = ({
|
||||
attemptInstall,
|
||||
attemptInstallByInfo,
|
||||
extensionInstallationStateStore,
|
||||
}: Dependencies): InstallFromInput => (
|
||||
async (input) => {
|
||||
let disposer: ExtendableDisposer | undefined = undefined;
|
||||
|
||||
try {
|
||||
// fixme: improve error messages for non-tar-file URLs
|
||||
if (InputValidators.isUrl.validate(input)) {
|
||||
// install via url
|
||||
disposer = extensionInstallationStateStore.startPreInstall();
|
||||
const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 });
|
||||
const fileName = path.basename(input);
|
||||
|
||||
return await attemptInstall({ fileName, dataP: promise }, disposer);
|
||||
}
|
||||
|
||||
try {
|
||||
await InputValidators.isPath.validate(input);
|
||||
|
||||
// install from system path
|
||||
const fileName = path.basename(input);
|
||||
|
||||
return await attemptInstall({ fileName, dataP: readFileNotify(input) });
|
||||
} catch (error) {
|
||||
const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input);
|
||||
|
||||
if (extNameCaptures) {
|
||||
const { name, version } = extNameCaptures;
|
||||
|
||||
return await attemptInstallByInfo({ name, version });
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unknown format of input: ${input}`);
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(`[EXTENSION-INSTALL]: installation has failed: ${message}`, { error, installPath: input });
|
||||
Notifications.error((
|
||||
<p>
|
||||
{"Installation has failed: "}
|
||||
<b>{message}</b>
|
||||
</p>
|
||||
));
|
||||
} finally {
|
||||
disposer?.();
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable";
|
||||
import loggerInjectable from "../../../../common/logger.injectable";
|
||||
import readFileBufferInjectable from "../../../../common/fs/read-file-buffer.injectable";
|
||||
|
||||
export type ReadFileNotify = (filePath: string, showError?: boolean) => Promise<Buffer | null>;
|
||||
|
||||
const readFileNotifyInjectable = getInjectable({
|
||||
id: "read-file-notify",
|
||||
instantiate: (di): ReadFileNotify => {
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const readFileBuffer = di.inject(readFileBufferInjectable);
|
||||
|
||||
return async (filePath, showError = true) => {
|
||||
try {
|
||||
return await readFileBuffer(filePath);
|
||||
} catch (error) {
|
||||
if (showError) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(`[EXTENSION-INSTALL]: preloading ${filePath} has failed: ${message}`, { error });
|
||||
showErrorNotification(`Error while reading "${filePath}": ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default readFileNotifyInjectable;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import fse from "fs-extra";
|
||||
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
||||
import logger from "../../../../main/logger";
|
||||
import { Notifications } from "../../notifications";
|
||||
|
||||
export const readFileNotify = async (filePath: string, showError = true): Promise<Buffer | null> => {
|
||||
try {
|
||||
return await fse.readFile(filePath);
|
||||
} catch (error) {
|
||||
if (showError) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(`[EXTENSION-INSTALL]: preloading ${filePath} has failed: ${message}`, { error });
|
||||
Notifications.error(`Error while reading "${filePath}": ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
Reference in new issue
Block a user