mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Change notification when extension is not found (#5184)
* Change notification when extension is not found - Removes legacy downloadFile and downloadJson in favour of using `node-fetch` - A notification will be displayed if the URL provided results in a status of 404 Signed-off-by: Sebastian Malton <sebastian@malton.name> * Resolve PR comments Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
da91347121
commit
f021d9d0db
56
src/common/fetch/download-binary.injectable.ts
Normal file
56
src/common/fetch/download-binary.injectable.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { RequestInit, Response } from "node-fetch";
|
||||
import type { AsyncResult } from "../utils/async-result";
|
||||
import fetchInjectable from "./fetch.injectable";
|
||||
|
||||
export interface DownloadBinaryOptions {
|
||||
signal?: AbortSignal | null | undefined;
|
||||
}
|
||||
|
||||
export type DownloadBinary = (url: string, opts?: DownloadBinaryOptions) => Promise<AsyncResult<Buffer, string>>;
|
||||
|
||||
const downloadBinaryInjectable = getInjectable({
|
||||
id: "download-binary",
|
||||
instantiate: (di): DownloadBinary => {
|
||||
const fetch = di.inject(fetchInjectable);
|
||||
|
||||
return async (url, opts) => {
|
||||
let result: Response;
|
||||
|
||||
try {
|
||||
// TODO: upgrade node-fetch once we switch to ESM
|
||||
result = await fetch(url, opts as RequestInit);
|
||||
} catch (error) {
|
||||
return {
|
||||
callWasSuccessful: false,
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
|
||||
if (result.status < 200 || 300 <= result.status) {
|
||||
return {
|
||||
callWasSuccessful: false,
|
||||
error: result.statusText,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
callWasSuccessful: true,
|
||||
response: await result.buffer(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
callWasSuccessful: false,
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default downloadBinaryInjectable;
|
||||
56
src/common/fetch/download-json.injectable.ts
Normal file
56
src/common/fetch/download-json.injectable.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { RequestInit, Response } from "node-fetch";
|
||||
import type { JsonValue } from "type-fest";
|
||||
import type { AsyncResult } from "../utils/async-result";
|
||||
import fetchInjectable from "./fetch.injectable";
|
||||
|
||||
export interface DownloadJsonOptions {
|
||||
signal?: AbortSignal | null | undefined;
|
||||
}
|
||||
|
||||
export type DownloadJson = (url: string, opts?: DownloadJsonOptions) => Promise<AsyncResult<JsonValue, string>>;
|
||||
|
||||
const downloadJsonInjectable = getInjectable({
|
||||
id: "download-json",
|
||||
instantiate: (di): DownloadJson => {
|
||||
const fetch = di.inject(fetchInjectable);
|
||||
|
||||
return async (url, opts) => {
|
||||
let result: Response;
|
||||
|
||||
try {
|
||||
result = await fetch(url, opts as RequestInit);
|
||||
} catch (error) {
|
||||
return {
|
||||
callWasSuccessful: false,
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
|
||||
if (result.status < 200 || 300 <= result.status) {
|
||||
return {
|
||||
callWasSuccessful: false,
|
||||
error: result.statusText,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
callWasSuccessful: true,
|
||||
response: await result.json(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
callWasSuccessful: false,
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default downloadJsonInjectable;
|
||||
@ -3,10 +3,10 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { RequestInfo, RequestInit, Response } from "node-fetch";
|
||||
import fetch from "node-fetch";
|
||||
import type { RequestInit, Response } from "node-fetch";
|
||||
|
||||
export type Fetch = (url: RequestInfo, init?: RequestInit) => Promise<Response>;
|
||||
export type Fetch = (url: string, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
const fetchInjectable = getInjectable({
|
||||
id: "fetch",
|
||||
|
||||
17
src/common/fetch/timeout-controller.ts
Normal file
17
src/common/fetch/timeout-controller.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates an AbortController with an associated timeout
|
||||
* @param timeout The number of milliseconds before this controller will auto abort
|
||||
*/
|
||||
export function withTimeout(timeout: number): AbortController {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
controller.signal.addEventListener("abort", () => clearTimeout(id));
|
||||
|
||||
return controller;
|
||||
}
|
||||
@ -6,7 +6,8 @@
|
||||
// Convert object's keys to camelCase format
|
||||
import { camelCase } from "lodash";
|
||||
import type { SingleOrMany } from "./types";
|
||||
import { isObject } from "./type-narrowing";
|
||||
import { isObject, isString } from "./type-narrowing";
|
||||
import * as object from "./objects";
|
||||
|
||||
export function toCamelCase<T extends Record<string, unknown>[]>(obj: T): T;
|
||||
export function toCamelCase<T extends Record<string, unknown>>(obj: T): T;
|
||||
@ -17,11 +18,10 @@ export function toCamelCase(obj: SingleOrMany<Record<string, unknown> | unknown>
|
||||
}
|
||||
|
||||
if (isObject(obj)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj)
|
||||
.map(([key, value]) => {
|
||||
return [camelCase(key), toCamelCase(value)];
|
||||
}),
|
||||
return object.fromEntries(
|
||||
object.entries(obj)
|
||||
.filter((pair): pair is [string, unknown] => isString(pair[0]))
|
||||
.map(([key, value]) => [camelCase(key), isObject(value) ? toCamelCase(value) : value]),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import request from "request";
|
||||
import type { JsonValue } from "type-fest";
|
||||
import { parse } from "./json";
|
||||
|
||||
export interface DownloadFileOptions {
|
||||
url: string;
|
||||
gzip?: boolean;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export interface DownloadFileTicket<T> {
|
||||
url: string;
|
||||
promise: Promise<T>;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions): DownloadFileTicket<Buffer> {
|
||||
const fileChunks: Buffer[] = [];
|
||||
const req = request(url, { gzip, timeout });
|
||||
const promise: Promise<Buffer> = new Promise((resolve, reject) => {
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
fileChunks.push(chunk);
|
||||
});
|
||||
req.once("error", err => {
|
||||
reject({ url, err });
|
||||
});
|
||||
req.once("complete", () => {
|
||||
resolve(Buffer.concat(fileChunks));
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
url,
|
||||
promise,
|
||||
cancel() {
|
||||
req.abort();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function downloadJson(args: DownloadFileOptions): DownloadFileTicket<JsonValue> {
|
||||
const { promise, ...rest } = downloadFile(args);
|
||||
|
||||
return {
|
||||
promise: promise.then(res => parse(res.toString())),
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
@ -14,7 +14,6 @@ export * from "./convertMemory";
|
||||
export * from "./debouncePromise";
|
||||
export * from "./delay";
|
||||
export * from "./disposer";
|
||||
export * from "./downloadFile";
|
||||
export * from "./escapeRegExp";
|
||||
export * from "./formatDuration";
|
||||
export * from "./getRandId";
|
||||
|
||||
@ -103,7 +103,7 @@ export function isBoolean(val: unknown): val is boolean {
|
||||
* checks if val is of type object and isn't null
|
||||
* @param val the value to be checked
|
||||
*/
|
||||
export function isObject(val: unknown): val is object {
|
||||
export function isObject(val: unknown): val is Record<string | symbol | number, unknown> {
|
||||
return typeof val === "object" && val !== null;
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,8 @@
|
||||
import type { RenderResult } from "@testing-library/react";
|
||||
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
|
||||
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
|
||||
import downloadBinaryInjectable, { type DownloadBinary } from "../../common/fetch/download-binary.injectable";
|
||||
import downloadJsonInjectable, { type DownloadJson } from "../../common/fetch/download-json.injectable";
|
||||
import focusWindowInjectable from "../../renderer/navigation/focus-window.injectable";
|
||||
|
||||
// TODO: Make components free of side effects by making them deterministic
|
||||
@ -15,14 +17,20 @@ describe("extensions - navigation using application menu", () => {
|
||||
let builder: ApplicationBuilder;
|
||||
let rendered: RenderResult;
|
||||
let focusWindowMock: jest.Mock;
|
||||
let downloadJson: jest.MockedFunction<DownloadJson>;
|
||||
let downloadBinary: jest.MockedFunction<DownloadBinary>;
|
||||
|
||||
beforeEach(async () => {
|
||||
builder = getApplicationBuilder();
|
||||
|
||||
builder.beforeWindowStart((windowDi) => {
|
||||
focusWindowMock = jest.fn();
|
||||
downloadJson = jest.fn().mockImplementation((url) => { throw new Error(`Unexpected call to downloadJson for url=${url}`); });
|
||||
downloadBinary = jest.fn().mockImplementation((url) => { throw new Error(`Unexpected call to downloadJson for url=${url}`); });
|
||||
|
||||
windowDi.override(focusWindowInjectable, () => focusWindowMock);
|
||||
windowDi.override(downloadJsonInjectable, () => downloadJson);
|
||||
windowDi.override(downloadBinaryInjectable, () => downloadBinary);
|
||||
});
|
||||
|
||||
rendered = await builder.render();
|
||||
|
||||
@ -25,23 +25,10 @@ import extensionInstallationStateStoreInjectable from "../../../../extensions/ex
|
||||
import { observable, when } from "mobx";
|
||||
import type { DeleteFile } from "../../../../common/fs/delete-file.injectable";
|
||||
import deleteFileInjectable from "../../../../common/fs/delete-file.injectable";
|
||||
|
||||
jest.mock("../../notifications");
|
||||
|
||||
jest.mock("../../../../common/utils/downloadFile", () => ({
|
||||
downloadFile: jest.fn(({ url }) => ({
|
||||
promise: Promise.resolve(),
|
||||
url,
|
||||
cancel: () => {},
|
||||
})),
|
||||
downloadJson: jest.fn(({ url }) => ({
|
||||
promise: Promise.resolve({}),
|
||||
url,
|
||||
cancel: () => { },
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../common/utils/tar");
|
||||
import type { DownloadJson } from "../../../../common/fetch/download-json.injectable";
|
||||
import type { DownloadBinary } from "../../../../common/fetch/download-binary.injectable";
|
||||
import downloadJsonInjectable from "../../../../common/fetch/download-json.injectable";
|
||||
import downloadBinaryInjectable from "../../../../common/fetch/download-binary.injectable";
|
||||
|
||||
describe("Extensions", () => {
|
||||
let extensionLoader: ExtensionLoader;
|
||||
@ -50,6 +37,8 @@ describe("Extensions", () => {
|
||||
let extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
let render: DiRender;
|
||||
let deleteFileMock: jest.MockedFunction<DeleteFile>;
|
||||
let downloadJson: jest.MockedFunction<DownloadJson>;
|
||||
let downloadBinary: jest.MockedFunction<DownloadBinary>;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
@ -65,6 +54,12 @@ describe("Extensions", () => {
|
||||
deleteFileMock = jest.fn();
|
||||
di.override(deleteFileInjectable, () => deleteFileMock);
|
||||
|
||||
downloadJson = jest.fn().mockImplementation((url) => { throw new Error(`Unexpected call to downloadJson for url=${url}`); });
|
||||
di.override(downloadJsonInjectable, () => downloadJson);
|
||||
|
||||
downloadBinary = jest.fn().mockImplementation((url) => { throw new Error(`Unexpected call to downloadJson for url=${url}`); });
|
||||
di.override(downloadBinaryInjectable, () => downloadBinary);
|
||||
|
||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||
@ -110,7 +105,7 @@ describe("Extensions", () => {
|
||||
// Approve confirm dialog
|
||||
fireEvent.click(await screen.findByText("Yes"));
|
||||
|
||||
await waitFor(() => {
|
||||
await waitFor(async () => {
|
||||
expect(extensionDiscovery.uninstallExtension).toHaveBeenCalled();
|
||||
fireEvent.click(menuTrigger);
|
||||
expect(screen.getByText("Disable")).toHaveAttribute("aria-disabled", "true");
|
||||
@ -124,6 +119,7 @@ describe("Extensions", () => {
|
||||
render(<Extensions />);
|
||||
|
||||
const resolveInstall = observable.box(false);
|
||||
const url = "https://test.extensionurl/package.tgz";
|
||||
|
||||
deleteFileMock.mockReturnValue(Promise.resolve());
|
||||
installExtensionFromInput.mockImplementation(async (input) => {
|
||||
@ -139,13 +135,26 @@ describe("Extensions", () => {
|
||||
exact: false,
|
||||
}), {
|
||||
target: {
|
||||
value: "https://test.extensionurl/package.tgz",
|
||||
value: url,
|
||||
},
|
||||
});
|
||||
|
||||
const doResolve = observable.box(false);
|
||||
|
||||
downloadBinary.mockImplementation(async (targetUrl) => {
|
||||
expect(targetUrl).toBe(url);
|
||||
|
||||
await when(() => doResolve.get());
|
||||
|
||||
return {
|
||||
callWasSuccessful: false,
|
||||
error: "unknown location",
|
||||
};
|
||||
});
|
||||
|
||||
fireEvent.click(await screen.findByText("Install"));
|
||||
expect((await screen.findByText("Install")).closest("button")).toBeDisabled();
|
||||
resolveInstall.set(true);
|
||||
doResolve.set(true);
|
||||
});
|
||||
|
||||
it("displays spinner while extensions are loading", () => {
|
||||
|
||||
@ -2,8 +2,7 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { downloadFile, downloadJson } from "../../../common/utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import { isObject } from "../../../common/utils";
|
||||
import React from "react";
|
||||
import { SemVer } from "semver";
|
||||
import URLParse from "url-parse";
|
||||
@ -14,6 +13,12 @@ import extensionInstallationStateStoreInjectable from "../../../extensions/exten
|
||||
import confirmInjectable from "../confirm-dialog/confirm.injectable";
|
||||
import { reduce } from "lodash";
|
||||
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
||||
import { withTimeout } from "../../../common/fetch/timeout-controller";
|
||||
import downloadBinaryInjectable from "../../../common/fetch/download-binary.injectable";
|
||||
import downloadJsonInjectable from "../../../common/fetch/download-json.injectable";
|
||||
import type { PackageJson } from "type-fest";
|
||||
import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
|
||||
export interface ExtensionInfo {
|
||||
name: string;
|
||||
@ -21,6 +26,19 @@ export interface ExtensionInfo {
|
||||
requireConfirmation?: boolean;
|
||||
}
|
||||
|
||||
interface NpmPackageVersionDescriptor extends PackageJson {
|
||||
dist: {
|
||||
integrity: string;
|
||||
shasum: string;
|
||||
tarball: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface NpmRegistryPackageDescriptor {
|
||||
versions: Partial<Record<string, NpmPackageVersionDescriptor>>;
|
||||
"dist-tags"?: Partial<Record<string, string>>;
|
||||
}
|
||||
|
||||
export type AttemptInstallByInfo = (info: ExtensionInfo) => Promise<void>;
|
||||
|
||||
const attemptInstallByInfoInjectable = getInjectable({
|
||||
@ -31,55 +49,95 @@ const attemptInstallByInfoInjectable = getInjectable({
|
||||
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||
const confirm = di.inject(confirmInjectable);
|
||||
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
|
||||
const downloadJson = di.inject(downloadJsonInjectable);
|
||||
const downloadBinary = di.inject(downloadBinaryInjectable);
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
return async (info) => {
|
||||
const { name, version, requireConfirmation = false } = info;
|
||||
const { name, version: versionOrTagName, 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;
|
||||
let json: NpmRegistryPackageDescriptor;
|
||||
|
||||
try {
|
||||
json = await downloadJson({ url: registryUrl }).promise;
|
||||
const result = await downloadJson(registryUrl);
|
||||
|
||||
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}`);
|
||||
if (!result.callWasSuccessful) {
|
||||
showErrorNotification(`Failed to get registry information for extension: ${result.error}`);
|
||||
|
||||
return disposer();
|
||||
}
|
||||
|
||||
if (!isObject(result.response) || Array.isArray(result.response)) {
|
||||
showErrorNotification("Failed to get registry information for extension");
|
||||
|
||||
return disposer();
|
||||
}
|
||||
|
||||
if (result.response.error || !isObject(result.response.versions)) {
|
||||
const message = result.response.error ? `: ${result.response.error}` : "";
|
||||
|
||||
showErrorNotification(`Failed to get registry information for extension${message}`);
|
||||
|
||||
return disposer();
|
||||
}
|
||||
|
||||
json = result.response as unknown as NpmRegistryPackageDescriptor;
|
||||
} 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");
|
||||
logger.warn("Set registry has invalid json", { url: baseUrl }, error);
|
||||
showErrorNotification("Failed to get valid registry information for 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}`);
|
||||
logger.error("Failed to download registry information", error);
|
||||
showErrorNotification(`Failed to get valid registry information for extension. ${error}`);
|
||||
}
|
||||
|
||||
return disposer();
|
||||
}
|
||||
|
||||
if (version) {
|
||||
if (!json.versions[version]) {
|
||||
if (json["dist-tags"][version]) {
|
||||
finalVersion = json["dist-tags"][version];
|
||||
} else {
|
||||
Notifications.error((
|
||||
let version = versionOrTagName;
|
||||
|
||||
if (versionOrTagName) {
|
||||
validDistTagName:
|
||||
if (!json.versions[versionOrTagName]) {
|
||||
if (json["dist-tags"]) {
|
||||
const potentialVersion = json["dist-tags"][versionOrTagName];
|
||||
|
||||
if (potentialVersion) {
|
||||
if (!json.versions[potentialVersion]) {
|
||||
showErrorNotification((
|
||||
<p>
|
||||
{"The "}
|
||||
<em>{name}</em>
|
||||
{" extension does not have a version or tag "}
|
||||
<code>{version}</code>
|
||||
Configured registry claims to have tag
|
||||
{" "}
|
||||
<code>{versionOrTagName}</code>
|
||||
.
|
||||
{" "}
|
||||
But does not have version infomation for the reference.
|
||||
</p>
|
||||
));
|
||||
|
||||
return disposer();
|
||||
}
|
||||
|
||||
version = potentialVersion;
|
||||
break validDistTagName;
|
||||
}
|
||||
}
|
||||
|
||||
showErrorNotification((
|
||||
<p>
|
||||
{"The "}
|
||||
<em>{name}</em>
|
||||
{" extension does not have a version or tag "}
|
||||
<code>{versionOrTagName}</code>
|
||||
.
|
||||
</p>
|
||||
));
|
||||
|
||||
return disposer();
|
||||
}
|
||||
} else {
|
||||
const versions = Object.keys(json.versions)
|
||||
@ -89,26 +147,33 @@ const attemptInstallByInfoInjectable = getInjectable({
|
||||
|
||||
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}`);
|
||||
version = latestVersion?.format();
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
logger.error("No versions supplied for extension", { name });
|
||||
showErrorNotification(`No versions found for ${name}`);
|
||||
|
||||
return disposer();
|
||||
}
|
||||
|
||||
finalVersion = latestVersion.format();
|
||||
const versionInfo = json.versions[version];
|
||||
const tarballUrl = versionInfo?.dist.tarball;
|
||||
|
||||
if (!tarballUrl) {
|
||||
showErrorNotification("Configured registry has invalid data model. Please verify that it is like NPM's.");
|
||||
logger.warn(`[ATTEMPT-INSTALL-BY-INFO]: registry returned unexpected data, final version is ${version} but the versions object is missing .dist.tarball as a string`, versionInfo);
|
||||
|
||||
return disposer();
|
||||
}
|
||||
|
||||
if (requireConfirmation) {
|
||||
const proceed = await confirm({
|
||||
message: (
|
||||
<p>
|
||||
Are you sure you want to install
|
||||
{" "}
|
||||
{"Are you sure you want to install "}
|
||||
<b>
|
||||
{name}
|
||||
@
|
||||
{finalVersion}
|
||||
{`${name}@${version}`}
|
||||
</b>
|
||||
?
|
||||
</p>
|
||||
@ -122,12 +187,17 @@ const attemptInstallByInfoInjectable = getInjectable({
|
||||
}
|
||||
}
|
||||
|
||||
// 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 });
|
||||
const fileName = getBasenameOfPath(tarballUrl);
|
||||
const { signal } = withTimeout(10 * 60 * 1000);
|
||||
const request = await downloadBinary(tarballUrl, { signal });
|
||||
|
||||
return attemptInstall({ fileName, dataP }, disposer);
|
||||
if (!request.callWasSuccessful) {
|
||||
showErrorNotification(`Failed to download extension: ${request.error}`);
|
||||
|
||||
return disposer();
|
||||
}
|
||||
|
||||
return attemptInstall({ fileName, data: request.response }, disposer);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,30 +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 uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable";
|
||||
import { attemptInstall } from "./attempt-install";
|
||||
import unpackExtensionInjectable from "./unpack-extension/unpack-extension.injectable";
|
||||
import getExtensionDestFolderInjectable
|
||||
from "./get-extension-dest-folder/get-extension-dest-folder.injectable";
|
||||
import createTempFilesAndValidateInjectable from "./create-temp-files-and-validate/create-temp-files-and-validate.injectable";
|
||||
import extensionInstallationStateStoreInjectable
|
||||
from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
|
||||
const attemptInstallInjectable = getInjectable({
|
||||
id: "attempt-install",
|
||||
|
||||
instantiate: (di) =>
|
||||
attemptInstall({
|
||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||
uninstallExtension: di.inject(uninstallExtensionInjectable),
|
||||
unpackExtension: di.inject(unpackExtensionInjectable),
|
||||
createTempFilesAndValidate: di.inject(createTempFilesAndValidateInjectable),
|
||||
getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default attemptInstallInjectable;
|
||||
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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 uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable";
|
||||
import type { UnpackExtension } from "./unpack-extension.injectable";
|
||||
import unpackExtensionInjectable from "./unpack-extension.injectable";
|
||||
import type { GetExtensionDestFolder } from "./get-extension-dest-folder.injectable";
|
||||
import getExtensionDestFolderInjectable from "./get-extension-dest-folder.injectable";
|
||||
import type { CreateTempFilesAndValidate } from "./create-temp-files-and-validate.injectable";
|
||||
import createTempFilesAndValidateInjectable from "./create-temp-files-and-validate.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import type { Disposer } from "../../../../common/utils";
|
||||
import { disposer } from "../../../../common/utils";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { Button } from "../../button";
|
||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
import type { LensExtensionId } from "../../../../extensions/lens-extension";
|
||||
import React from "react";
|
||||
import { remove as removeDir } from "fs-extra";
|
||||
import { shell } from "electron";
|
||||
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";
|
||||
|
||||
export interface InstallRequest {
|
||||
fileName: string;
|
||||
data: Buffer;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
uninstallExtension: (id: LensExtensionId) => Promise<boolean>;
|
||||
unpackExtension: UnpackExtension;
|
||||
createTempFilesAndValidate: CreateTempFilesAndValidate;
|
||||
getExtensionDestFolder: GetExtensionDestFolder;
|
||||
installStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
export type AttemptInstall = (request: InstallRequest, cleanup?: Disposer) => Promise<void>;
|
||||
|
||||
const attemptInstall = ({
|
||||
extensionLoader,
|
||||
uninstallExtension,
|
||||
unpackExtension,
|
||||
createTempFilesAndValidate,
|
||||
getExtensionDestFolder,
|
||||
installStateStore,
|
||||
}: Dependencies): AttemptInstall =>
|
||||
async (request, cleanup) => {
|
||||
const dispose = disposer(
|
||||
installStateStore.startPreInstall(),
|
||||
cleanup,
|
||||
);
|
||||
|
||||
const validatedRequest = await createTempFilesAndValidate(request);
|
||||
|
||||
if (!validatedRequest) {
|
||||
return dispose();
|
||||
}
|
||||
|
||||
const { name, version, description } = validatedRequest.manifest;
|
||||
const curState = installStateStore.getInstallationState(validatedRequest.id);
|
||||
|
||||
if (curState !== ExtensionInstallationState.IDLE) {
|
||||
dispose();
|
||||
|
||||
return void Notifications.error(
|
||||
<div className="flex column gaps">
|
||||
<b>Extension Install Collision:</b>
|
||||
<p>
|
||||
{"The "}
|
||||
<em>{name}</em>
|
||||
{` extension is currently ${curState.toLowerCase()}.`}
|
||||
</p>
|
||||
<p>Will not proceed with this current install request.</p>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
const extensionFolder = getExtensionDestFolder(name);
|
||||
const installedExtension = extensionLoader.getExtension(validatedRequest.id);
|
||||
|
||||
if (installedExtension) {
|
||||
const { version: oldVersion } = installedExtension.manifest;
|
||||
|
||||
// confirm to uninstall old version before installing new version
|
||||
const removeNotification = Notifications.info(
|
||||
<div className="InstallingExtensionNotification flex gaps align-center">
|
||||
<div className="flex column gaps">
|
||||
<p>
|
||||
{"Install extension "}
|
||||
<b>{`${name}@${version}`}</b>
|
||||
?
|
||||
</p>
|
||||
<p>
|
||||
{"Description: "}
|
||||
<em>{description}</em>
|
||||
</p>
|
||||
<div
|
||||
className="remove-folder-warning"
|
||||
onClick={() => shell.openPath(extensionFolder)}
|
||||
>
|
||||
<b>Warning:</b>
|
||||
{` ${name}@${oldVersion} will be removed before installation.`}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
autoFocus
|
||||
label="Install"
|
||||
onClick={async () => {
|
||||
removeNotification();
|
||||
|
||||
if (await uninstallExtension(validatedRequest.id)) {
|
||||
await unpackExtension(validatedRequest, dispose);
|
||||
} else {
|
||||
dispose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
{
|
||||
onClose: dispose,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// clean up old data if still around
|
||||
await removeDir(extensionFolder);
|
||||
|
||||
// install extension if not yet exists
|
||||
await unpackExtension(validatedRequest, dispose);
|
||||
}
|
||||
};
|
||||
|
||||
const attemptInstallInjectable = getInjectable({
|
||||
id: "attempt-install",
|
||||
instantiate: (di) => attemptInstall({
|
||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||
uninstallExtension: di.inject(uninstallExtensionInjectable),
|
||||
unpackExtension: di.inject(unpackExtensionInjectable),
|
||||
createTempFilesAndValidate: di.inject(createTempFilesAndValidateInjectable),
|
||||
getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable),
|
||||
installStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default attemptInstallInjectable;
|
||||
@ -1,126 +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 { disposer } from "../../../../common/utils";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { Button } from "../../button";
|
||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
import type { LensExtensionId } from "../../../../extensions/lens-extension";
|
||||
import React from "react";
|
||||
import { remove as removeDir } from "fs-extra";
|
||||
import { shell } from "electron";
|
||||
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 { 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: UnpackExtension;
|
||||
createTempFilesAndValidate: CreateTempFilesAndValidate;
|
||||
getExtensionDestFolder: GetExtensionDestFolder;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
export const attemptInstall =
|
||||
({
|
||||
extensionLoader,
|
||||
uninstallExtension,
|
||||
unpackExtension,
|
||||
createTempFilesAndValidate,
|
||||
getExtensionDestFolder,
|
||||
extensionInstallationStateStore,
|
||||
}: Dependencies) =>
|
||||
async (request: InstallRequest, d?: ExtendableDisposer): Promise<void> => {
|
||||
console.log("Attempting to install extension");
|
||||
|
||||
const dispose = disposer(
|
||||
extensionInstallationStateStore.startPreInstall(),
|
||||
d,
|
||||
);
|
||||
|
||||
const validatedRequest = await createTempFilesAndValidate(request);
|
||||
|
||||
if (!validatedRequest) {
|
||||
return dispose();
|
||||
}
|
||||
|
||||
const { name, version, description } = validatedRequest.manifest;
|
||||
const curState = extensionInstallationStateStore.getInstallationState(
|
||||
validatedRequest.id,
|
||||
);
|
||||
|
||||
if (curState !== ExtensionInstallationState.IDLE) {
|
||||
dispose();
|
||||
|
||||
return void Notifications.error(
|
||||
<div className="flex column gaps">
|
||||
<b>Extension Install Collision:</b>
|
||||
<p>
|
||||
{"The "}
|
||||
<em>{name}</em>
|
||||
{` extension is currently ${curState.toLowerCase()}.`}
|
||||
</p>
|
||||
<p>Will not proceed with this current install request.</p>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
const extensionFolder = getExtensionDestFolder(name);
|
||||
const installedExtension = extensionLoader.getExtension(validatedRequest.id);
|
||||
|
||||
if (installedExtension) {
|
||||
const { version: oldVersion } = installedExtension.manifest;
|
||||
|
||||
// confirm to uninstall old version before installing new version
|
||||
const removeNotification = Notifications.info(
|
||||
<div className="InstallingExtensionNotification flex gaps align-center">
|
||||
<div className="flex column gaps">
|
||||
<p>
|
||||
{"Install extension "}
|
||||
<b>{`${name}@${version}`}</b>
|
||||
?
|
||||
</p>
|
||||
<p>
|
||||
{"Description: "}
|
||||
<em>{description}</em>
|
||||
</p>
|
||||
<div
|
||||
className="remove-folder-warning"
|
||||
onClick={() => shell.openPath(extensionFolder)}
|
||||
>
|
||||
<b>Warning:</b>
|
||||
{` ${name}@${oldVersion} will be removed before installation.`}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
autoFocus
|
||||
label="Install"
|
||||
onClick={async () => {
|
||||
removeNotification();
|
||||
|
||||
if (await uninstallExtension(validatedRequest.id)) {
|
||||
await unpackExtension(validatedRequest, dispose);
|
||||
} else {
|
||||
dispose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
{
|
||||
onClose: dispose,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// clean up old data if still around
|
||||
await removeDir(extensionFolder);
|
||||
|
||||
// install extension if not yet exists
|
||||
await unpackExtension(validatedRequest, dispose);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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 extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import { validatePackage } from "./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 "./attempt-install.injectable";
|
||||
|
||||
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 type CreateTempFilesAndValidate = (request: InstallRequest) => Promise<InstallRequestValidated | null>;
|
||||
|
||||
const createTempFilesAndValidate = ({
|
||||
extensionDiscovery,
|
||||
}: Dependencies): CreateTempFilesAndValidate => (
|
||||
async ({ fileName, data }) => {
|
||||
// copy files to temp
|
||||
await fse.ensureDir(getExtensionPackageTemp());
|
||||
|
||||
// validate packages
|
||||
const tempFile = getExtensionPackageTemp(fileName);
|
||||
|
||||
try {
|
||||
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);
|
||||
}
|
||||
|
||||
const createTempFilesAndValidateInjectable = getInjectable({
|
||||
id: "create-temp-files-and-validate",
|
||||
instantiate: (di) => createTempFilesAndValidate({
|
||||
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default createTempFilesAndValidateInjectable;
|
||||
@ -1,104 +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 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) => {
|
||||
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;
|
||||
@ -3,20 +3,19 @@
|
||||
* 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 extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import { sanitizeExtensionName } from "../../../../extensions/lens-extension";
|
||||
import path from "path";
|
||||
|
||||
export type GetExtensionDestFolder = (extensionName: string) => string;
|
||||
export type GetExtensionDestFolder = (name: string) => string;
|
||||
|
||||
const getExtensionDestFolderInjectable = getInjectable({
|
||||
id: "get-extension-dest-folder",
|
||||
|
||||
instantiate: (di): GetExtensionDestFolder => {
|
||||
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
|
||||
return (name) => joinPaths(extensionDiscovery.localFolderPath, sanitizeExtensionName(name));
|
||||
return (name) => path.join(extensionDiscovery.localFolderPath, sanitizeExtensionName(name));
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
export interface InstallRequest {
|
||||
fileName: string;
|
||||
dataP: Promise<Buffer | null>;
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 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 getExtensionDestFolderInjectable from "./get-extension-dest-folder.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import type { Disposer } from "../../../../common/utils";
|
||||
import { noop } from "../../../../common/utils";
|
||||
import { extensionDisplayName } from "../../../../extensions/lens-extension";
|
||||
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 { InstallRequestValidated } from "./create-temp-files-and-validate.injectable";
|
||||
import extractTarInjectable from "../../../../common/fs/extract-tar.injectable";
|
||||
import loggerInjectable from "../../../../common/logger.injectable";
|
||||
import showInfoNotificationInjectable from "../../notifications/show-info-notification.injectable";
|
||||
import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable";
|
||||
|
||||
export type UnpackExtension = (request: InstallRequestValidated, disposeDownloading?: Disposer) => Promise<void>;
|
||||
|
||||
const unpackExtensionInjectable = getInjectable({
|
||||
id: "unpack-extension",
|
||||
instantiate: (di): UnpackExtension => {
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
const getExtensionDestFolder = di.inject(getExtensionDestFolderInjectable);
|
||||
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||
const extractTar = di.inject(extractTarInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const showInfoNotification = di.inject(showInfoNotificationInjectable);
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
|
||||
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 = 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);
|
||||
|
||||
showInfoNotification((
|
||||
<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
|
||||
fse.remove(unpackingTempFolder).catch(noop);
|
||||
fse.unlink(tempFile).catch(noop);
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default unpackExtensionInjectable;
|
||||
@ -1,130 +0,0 @@
|
||||
/**
|
||||
* 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 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 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 => {
|
||||
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;
|
||||
@ -2,14 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { LensExtensionManifest } from "../../../../../extensions/lens-extension";
|
||||
import { hasTypedProperty, isObject, isString, listTarEntries, readFileFromTar } from "../../../../../common/utils";
|
||||
import { manifestFilename } from "../../../../../extensions/extension-discovery/extension-discovery";
|
||||
import type { LensExtensionManifest } from "../../../../extensions/lens-extension";
|
||||
import { hasTypedProperty, isObject, isString, listTarEntries, readFileFromTar } from "../../../../common/utils";
|
||||
import { manifestFilename } from "../../../../extensions/extension-discovery/extension-discovery";
|
||||
import path from "path";
|
||||
|
||||
export const validatePackage = async (
|
||||
filePath: string,
|
||||
): Promise<LensExtensionManifest> => {
|
||||
export async function validatePackage(filePath: string): Promise<LensExtensionManifest> {
|
||||
const tarFiles = await listTarEntries(filePath);
|
||||
|
||||
// tarball from npm contains single root folder "package/*"
|
||||
@ -20,9 +18,7 @@ export const validatePackage = async (
|
||||
}
|
||||
|
||||
const rootFolder = path.normalize(firstFile).split(path.sep)[0];
|
||||
const packedInRootFolder = tarFiles.every(entry =>
|
||||
entry.startsWith(rootFolder),
|
||||
);
|
||||
const packedInRootFolder = tarFiles.every(entry => entry.startsWith(rootFolder));
|
||||
const manifestLocation = packedInRootFolder
|
||||
? path.join(rootFolder, manifestFilename)
|
||||
: manifestFilename;
|
||||
@ -48,4 +44,4 @@ export const validatePackage = async (
|
||||
}
|
||||
|
||||
throw new Error(`${manifestFilename} must specify "main" and/or "renderer" fields`);
|
||||
};
|
||||
}
|
||||
@ -3,9 +3,9 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
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";
|
||||
import attemptInstallInjectable from "./attempt-install/attempt-install.injectable";
|
||||
import path from "path";
|
||||
import readFileNotifyInjectable from "./read-file-notify/read-file-notify.injectable";
|
||||
|
||||
export type AttemptInstalls = (filePaths: string[]) => Promise<void>;
|
||||
|
||||
@ -14,16 +14,23 @@ const attemptInstallsInjectable = getInjectable({
|
||||
|
||||
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),
|
||||
})
|
||||
)));
|
||||
await Promise.allSettled(
|
||||
filePaths.map(async filePath => {
|
||||
const data = await readFileNotify(filePath);
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
return attemptInstall({
|
||||
fileName: path.basename(filePath),
|
||||
data,
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -32,7 +32,8 @@ import type { InstallExtensionFromInput } from "./install-extension-from-input.i
|
||||
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 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";
|
||||
@ -44,7 +45,7 @@ interface Dependencies {
|
||||
confirmUninstallExtension: ConfirmUninstallExtension;
|
||||
installExtensionFromInput: InstallExtensionFromInput;
|
||||
installFromSelectFileDialog: () => Promise<void>;
|
||||
installOnDrop: (files: File[]) => Promise<void>;
|
||||
installOnDrop: InstallOnDrop;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
*/
|
||||
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";
|
||||
@ -15,6 +14,8 @@ import readFileNotifyInjectable from "./read-file-notify/read-file-notify.inject
|
||||
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
||||
import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import downloadBinaryInjectable from "../../../common/fetch/download-binary.injectable";
|
||||
import { withTimeout } from "../../../common/fetch/timeout-controller";
|
||||
|
||||
export type InstallExtensionFromInput = (input: string) => Promise<void>;
|
||||
|
||||
@ -29,6 +30,7 @@ const installExtensionFromInputInjectable = getInjectable({
|
||||
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const downloadBinary = di.inject(downloadBinaryInjectable);
|
||||
|
||||
return async (input) => {
|
||||
let disposer: ExtendableDisposer | undefined = undefined;
|
||||
@ -38,10 +40,18 @@ const installExtensionFromInputInjectable = getInjectable({
|
||||
if (InputValidators.isUrl.validate(input)) {
|
||||
// install via url
|
||||
disposer = extensionInstallationStateStore.startPreInstall();
|
||||
const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 });
|
||||
const { signal } = withTimeout(10 * 60 * 1000);
|
||||
const result = await downloadBinary(input, { signal });
|
||||
|
||||
if (!result.callWasSuccessful) {
|
||||
showErrorNotification(`Failed to download extension: ${result.error}`);
|
||||
|
||||
return disposer();
|
||||
}
|
||||
|
||||
const fileName = getBasenameOfPath(input);
|
||||
|
||||
return await attemptInstall({ fileName, dataP: promise }, disposer);
|
||||
return await attemptInstall({ fileName, data: result.response }, disposer);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -49,8 +59,13 @@ const installExtensionFromInputInjectable = getInjectable({
|
||||
|
||||
// install from system path
|
||||
const fileName = getBasenameOfPath(input);
|
||||
const data = await readFileNotify(input);
|
||||
|
||||
return await attemptInstall({ fileName, dataP: readFileNotify(input) });
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await attemptInstall({ fileName, data });
|
||||
} catch (error) {
|
||||
const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input);
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { requestOpenFilePickingDialog } from "../../ipc";
|
||||
import { supportedExtensionFormats } from "./supported-extension-formats";
|
||||
import attemptInstallsInjectable from "./attempt-installs/attempt-installs.injectable";
|
||||
import attemptInstallsInjectable from "./attempt-installs.injectable";
|
||||
import directoryForDownloadsInjectable from "../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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 loggerInjectable from "../../../common/logger.injectable";
|
||||
import attemptInstallsInjectable from "./attempt-installs.injectable";
|
||||
|
||||
export type InstallOnDrop = (files: File[]) => Promise<void>;
|
||||
|
||||
const installOnDropInjectable = getInjectable({
|
||||
id: "install-on-drop",
|
||||
|
||||
instantiate: (di): InstallOnDrop => {
|
||||
const attemptInstalls = di.inject(attemptInstallsInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
return (files) => {
|
||||
logger.info("Install from D&D");
|
||||
|
||||
return attemptInstalls(files.map(({ path }) => path));
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default installOnDropInjectable;
|
||||
@ -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 { installOnDrop } from "./install-on-drop";
|
||||
import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable";
|
||||
|
||||
const installOnDropInjectable = getInjectable({
|
||||
id: "install-on-drop",
|
||||
|
||||
instantiate: (di) =>
|
||||
installOnDrop({
|
||||
attemptInstalls: di.inject(attemptInstallsInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default installOnDropInjectable;
|
||||
@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import logger from "../../../../main/logger";
|
||||
|
||||
interface Dependencies {
|
||||
attemptInstalls: (filePaths: string[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export const installOnDrop =
|
||||
({ attemptInstalls }: Dependencies) =>
|
||||
async (files: File[]) => {
|
||||
logger.info("Install from D&D");
|
||||
await attemptInstalls(files.map(({ path }) => path));
|
||||
};
|
||||
@ -56,9 +56,7 @@ const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({
|
||||
<div>
|
||||
<Input
|
||||
theme="round-black"
|
||||
disabled={
|
||||
extensionInstallationStateStore.anyPreInstallingOrInstalling
|
||||
}
|
||||
disabled={extensionInstallationStateStore.anyPreInstallingOrInstalling}
|
||||
placeholder={"Name or file path or URL"}
|
||||
showErrorsAsTooltip={{ preferredPositions: TooltipPosition.BOTTOM }}
|
||||
validators={installPath ? installInputValidator : undefined}
|
||||
|
||||
@ -7,6 +7,7 @@ import "./input.scss";
|
||||
|
||||
import type { DOMAttributes, InputHTMLAttributes, TextareaHTMLAttributes } from "react";
|
||||
import React from "react";
|
||||
import type { SingleOrMany } from "../../utils";
|
||||
import { autoBind, cssNames, debouncePromise, getRandId, isPromiseSettledFulfilled } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import type { TooltipProps } from "../tooltip";
|
||||
@ -72,7 +73,7 @@ export type InputProps = Omit<InputElementProps, "onChange" | "onSubmit"> & {
|
||||
iconLeft?: IconData;
|
||||
iconRight?: IconData;
|
||||
contentRight?: string | React.ReactNode; // Any component of string goes after iconRight
|
||||
validators?: InputValidator<boolean> | InputValidator<boolean>[];
|
||||
validators?: SingleOrMany<InputValidator>;
|
||||
blurOnEnter?: boolean;
|
||||
onChange?(value: string, evt: React.ChangeEvent<InputElement>): void;
|
||||
onSubmit?(value: string, evt: React.KeyboardEvent<InputElement>): void;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user