Turn on strict mode in tsconfig.json, some helpful lints, and required cleanup where strictness necessitates it (#5195)

* Turn on strict mode in tsconfig.json

- Add route, clusterRoute, and payloadValidatedClusterRoute helper
  functions to improve types with backend routes

- Turn on the following new lints:
  - react/jsx-first-prop-new-line
  - react/jsx-wrap-multilines
  - react/jsx-one-expression-per-line
  - react/jsx-max-props-per-line
  - react/jsx-indent
  - react/jsx-indent-props

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix build

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace KubeObject scope strings with enum

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Revert package.json version changes

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* revert move hostedCluster(Id)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* change some type param names to be not single letters

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove copy-extension-themes

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* add new make clean action

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix build to not use webpack for generating types

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kube-object-menu.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix select.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix catalog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* revert move fileNameMigration to index

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix ref logic error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix log-resource-selector.test.tsx tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix dock-store.test.ts test by overriding createStorage to not touch file system

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix cluster.test.ts tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kube=api.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fixed hotbar-store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kubeconfig-manager.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix cluster-role-bindings/__tests__/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix role-bindings/__tests__/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix pods.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix delete-cluster-dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix daemonset.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix replicaset.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix statefulsets/dialog/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix replicasets/scale-dialog/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix deployments.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix deployments/scale/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix cronjob.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix stateful-set.api.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix deployment.api.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix api-manager.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix statefulset.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix job.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix pods.store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix scroll-spy.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix hotbar-remove-command.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix catalog-entity-registry.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix welcome.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix verify-that-all-routes-have-route-component.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix pod-tolerations.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* better fix for previous 3 fixes, plus also select.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kube-object-menu.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix app-paths.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix dock-tabs.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix isReactNode typing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix sub-title.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix drawer.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix list-layout.tsx and header.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix error-boundary.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix upgrade-chart/store.ts and dock-tab.store.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix install-chart/store.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix edit-resource/store.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix create-resource/store.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix namespace-select.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix namespace-select-filter.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix crd-list.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix wrong types for extensions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix circular dependency

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix circular dependency on catalogCategoryRegistry

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix api-kube

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix type errors, most <Select /> errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fixing more type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* some more fixing type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* convert all KubeApis to injectable with legacy global backups

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* factor out into a common file all the exports

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* convert all KubeObjectStores to injectable with legacy global backups

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix lint

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove unused legacy KubeApi globals

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix bad previous commit

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* more crash fixing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* try and fix behavioural tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix sidebar-and-tab-navigation-for-core.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix sidebar-and-tab-navigation-for-extensions.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-using-application-menu.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix catalog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make ThemeStore non-singleton and fix navigation-to-terminal-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* extensions.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix catalog-entity-registry.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-using-application-menu.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix log-resource-selector.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix dock-tabs.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix delete-cluster-dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-kubernetes-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-editor-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-proxy-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-using-application-menu.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-application-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix dock-store.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix select.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix role-bindings/__tests__/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix hotbar-remove-command.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix cluster-role-bindings/__tests__/dialog.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-extension-specific-preferences.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-telemetry-preferences.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix closing-preferences.test.tsx

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-editor-preferences.test.ts

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix navigation-to-proxy-preferences.test.ts

- Fix other type errors too

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* final tweaks

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add more tsconfig files, fix bug in <Catalog>

- Make all of history, navigation injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert all of kube-details-params/ and navigate/ to injectable

- This fixes a runtime error that was encountered during testing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix runtime errors on renderer

- remove all static uses of `createPageParam` (and then removed the
  legacy global)
- Made LensRendererExtension and LensMainExtension just used
  dependencies and not the getLegacyDi
- Fixed circular dep in extension-loader

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move registerStore calls to after injectMany

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* replace all the rest of the legacy uses of apiManager

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix stack overflow and cycles in DI

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix NamespaceSelectFilter not opening

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix WizardStep and AddNamespaceDialog

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix KubeApi's not being registered

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* cleanup WindowManager

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Proper fix for Wizard, fix NamespaceStore.subscribe

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Rewrite withTooltip to be more type correct

- Fixes mobx related "too many recursive actions" error

- Change all the uses of withTooltips to be functional components

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add e2e test to cover kube api registration

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* cleanup internal-commands

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove cast in <Animate>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix command-palette e2e test

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type error after rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix test name

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix lint

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix code to help CodeQL scanner

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* update intree extension lock files

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix build-extensions picking wrong @types/react

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix tests from rebase

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix type error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make KubeconfigSyncManager more injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix crash in test mode for Dialog

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* make Select snapshots deterministic

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix new type error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix kube-object.store.test.ts typing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix merge build issues

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix snapshots after merge

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix lint after merge

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* reexport BaseKubeJsonApiObjectMetadata

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix typo in terminalSpawningPool

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove duplicate license header

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix typo to waitUntilDefined

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove iter use from getLegacyGlobalDiForExtensionApi

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove complex createStorage override

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* override logger with mocks only when needed for tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove specialized overrideStore flags for getDiForUnitTesting

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove unnecessary | undefined types from the exactOptionalFieldTypes experiment

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* use more descriptive name for local test mocks

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove unnecessary addition to 'make clean' target

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove oddity of KubeObjectStore.getById(undefined) being allowed

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* rename KubeObject.getDescriptor in favour of name without fundemental JS meaning

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Simplify legacyRegisterApi when working in behaviour unit tests

- Don't emit within main environment as there should be no auto
  registering there

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* change confusing variable name in ReactiveDuration

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* make visitor pattern more explicit for Entity contextMenuOpen

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* toggleDetails -> toggleKubeDetailsPane is more specific

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* remove outdated comment

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix bug where LensExtension dependencies are not set

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests from the revert of react 18

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix more tests from merge

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix typings with new is-compatible-extension tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* more type fixing

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Revert in-tree extension versions

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Improve name of guarding injectable for stores and apis

- New name better implies that it is just a guard state and does not do
  anything

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add helper for <Select>.isMulti for storing in a Set<Value>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* fix is-compatible-extension.test.ts types

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2022-05-16 07:17:57 -04:00
1 parent 381d77c633
commit dfcb7c3330
1062 files changed
+26512 -28689

No files matched your search

@@ -4,7 +4,7 @@
*/
import "@testing-library/jest-dom/extend-expect";
import { fireEvent, waitFor } from "@testing-library/react";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import fse from "fs-extra";
import React from "react";
import { UserStore } from "../../../../common/user-store";
@@ -21,10 +21,14 @@ 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 appVersionInjectable
from "../../../../common/get-configuration-file-model/app-version/app-version.injectable";
import getConfigurationFileModelInjectable from "../../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../../../../common/get-configuration-file-model/app-version/app-version.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 { 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";
mockWindow();
@@ -50,6 +54,8 @@ jest.mock("../../../../common/utils/tar");
describe("Extensions", () => {
let extensionLoader: ExtensionLoader;
let extensionDiscovery: ExtensionDiscovery;
let installFromInput: jest.MockedFunction<InstallFromInput>;
let extensionInstallationStateStore: ExtensionInstallationStateStore;
let render: DiRender;
beforeEach(async () => {
@@ -69,8 +75,13 @@ describe("Extensions", () => {
render = renderFor(di);
installFromInput = jest.fn();
di.override(installFromInputInjectable, () => installFromInput);
extensionLoader = di.inject(extensionLoaderInjectable);
extensionDiscovery = di.inject(extensionDiscoveryInjectable);
extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
extensionLoader.addExtension({
id: "extensionId",
@@ -99,36 +110,53 @@ describe("Extensions", () => {
it("disables uninstall and disable buttons while uninstalling", async () => {
extensionDiscovery.isLoaded = true;
const res = render(<><Extensions /><ConfirmDialog /></>);
const table = res.getByTestId("extensions-table");
const menuTrigger = table.querySelector("div[role=row]:first-of-type .actions .Icon");
render((
<>
<Extensions />
<ConfirmDialog />
</>
));
const table = await screen.findByTestId("extensions-table");
const menuTrigger = table.querySelector(".table div[role='rowgroup'] .actions .Icon");
assert(menuTrigger);
fireEvent.click(menuTrigger);
expect(res.getByText("Disable")).toHaveAttribute("aria-disabled", "false");
expect(res.getByText("Uninstall")).toHaveAttribute("aria-disabled", "false");
expect(await screen.findByText("Disable")).toHaveAttribute("aria-disabled", "false");
expect(await screen.findByText("Uninstall")).toHaveAttribute("aria-disabled", "false");
fireEvent.click(res.getByText("Uninstall"));
fireEvent.click(await screen.findByText("Uninstall"));
// Approve confirm dialog
fireEvent.click(res.getByText("Yes"));
fireEvent.click(await screen.findByText("Yes"));
await waitFor(() => {
expect(extensionDiscovery.uninstallExtension).toHaveBeenCalled();
fireEvent.click(menuTrigger);
expect(res.getByText("Disable")).toHaveAttribute("aria-disabled", "true");
expect(res.getByText("Uninstall")).toHaveAttribute("aria-disabled", "true");
expect(screen.getByText("Disable")).toHaveAttribute("aria-disabled", "true");
expect(screen.getByText("Uninstall")).toHaveAttribute("aria-disabled", "true");
}, {
timeout: 30000,
});
});
it("disables install button while installing", async () => {
const res = render(<Extensions />);
render(<Extensions />);
(fse.unlink as jest.MockedFunction<typeof fse.unlink>).mockReturnValue(Promise.resolve() as any);
const resolveInstall = observable.box(false);
fireEvent.change(res.getByPlaceholderText("File path or URL", {
(fse.unlink as jest.MockedFunction<typeof fse.unlink>).mockReturnValue(Promise.resolve());
installFromInput.mockImplementation(async (input) => {
expect(input).toBe("https://test.extensionurl/package.tgz");
const clear = extensionInstallationStateStore.startPreInstall();
await when(() => resolveInstall.get());
clear();
});
fireEvent.change(await screen.findByPlaceholderText("File path or URL", {
exact: false,
}), {
target: {
@@ -136,8 +164,9 @@ describe("Extensions", () => {
},
});
fireEvent.click(res.getByText("Install"));
expect(res.getByText("Install").closest("button")).toBeDisabled();
fireEvent.click(await screen.findByText("Install"));
expect((await screen.findByText("Install")).closest("button")).toBeDisabled();
resolveInstall.set(true);
});
it("displays spinner while extensions are loading", () => {
@@ -10,7 +10,6 @@ import path from "path";
import { SemVer } from "semver";
import URLParse from "url-parse";
import type { InstallRequest } from "./attempt-install/install-request";
import { reduce } from "lodash";
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";
@@ -18,6 +17,7 @@ import attemptInstallInjectable from "./attempt-install/attempt-install.injectab
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";
export interface ExtensionInfo {
name: string;
@@ -78,7 +78,11 @@ const attemptInstallByInfo = ({
} else {
Notifications.error((
<p>
The <em>{name}</em> extension does not have a version or tag <code>{version}</code>.
{"The "}
<em>{name}</em>
{" extension does not have a version or tag "}
<code>{version}</code>
.
</p>
));
@@ -91,19 +95,28 @@ const attemptInstallByInfo = ({
// ignore pre-releases for auto picking the version
.filter(version => version.prerelease.length === 0);
finalVersion = reduce(
versions,
(prev, curr) => prev.compareMain(curr) === -1 ? curr : prev,
).format();
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}`);
return disposer();
}
finalVersion = latestVersion.format();
}
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}
@
{finalVersion}
</b>
?
</p>
@@ -117,7 +130,8 @@ const attemptInstallByInfo = ({
}
}
const url = json.versions[finalVersion].dist.tarball;
// 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 });
@@ -74,7 +74,9 @@ export const attemptInstall =
<div className="flex column gaps">
<b>Extension Install Collision:</b>
<p>
The <em>{name}</em> extension is currently {curState.toLowerCase()}.
{"The "}
<em>{name}</em>
{` extension is currently ${curState.toLowerCase()}.`}
</p>
<p>Will not proceed with this current install request.</p>
</div>,
@@ -92,21 +94,20 @@ export const attemptInstall =
<div className="InstallingExtensionNotification flex gaps align-center">
<div className="flex column gaps">
<p>
Install extension{" "}
<b>
{name}@{version}
</b>
?
{"Install extension "}
<b>{`${name}@${version}`}</b>
?
</p>
<p>
Description: <em>{description}</em>
{"Description: "}
<em>{description}</em>
</p>
<div
className="remove-folder-warning"
onClick={() => shell.openPath(extensionFolder)}
>
<b>Warning:</b> {name}@{oldVersion} will be removed before
installation.
<b>Warning:</b>
{` ${name}@${oldVersion} will be removed before installation.`}
</div>
</div>
<Button
@@ -73,10 +73,13 @@ export const createTempFilesAndValidate =
Notifications.error(
<div className="flex column gaps">
<p>
Installing <em>{fileName}</em> has failed, skipping.
{"Installing "}
<em>{fileName}</em>
{" has failed, skipping."}
</p>
<p>
Reason: <em>{message}</em>
{"Reason: "}
<em>{message}</em>
</p>
</div>,
);
@@ -75,7 +75,9 @@ export const unpackExtension =
Notifications.ok(
<p>
Extension <b>{displayName}</b> successfully installed!
{"Extension "}
<b>{displayName}</b>
{" successfully installed!"}
</p>,
);
} catch (error) {
@@ -87,7 +89,9 @@ export const unpackExtension =
);
Notifications.error(
<p>
Installing extension <b>{displayName}</b> has failed:{" "}
{"Installing extension "}
<b>{displayName}</b>
{" has failed: "}
<em>{message}</em>
</p>,
);
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { LensExtensionManifest } from "../../../../../extensions/lens-extension";
import { listTarEntries, readFileFromTar } from "../../../../../common/utils";
import { hasTypedProperty, isObject, isString, listTarEntries, readFileFromTar } from "../../../../../common/utils";
import { manifestFilename } from "../../../../../extensions/extension-discovery/extension-discovery";
import path from "path";
@@ -31,17 +31,21 @@ export const validatePackage = async (
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
}
const manifest = await readFileFromTar<LensExtensionManifest>({
const manifest = await readFileFromTar({
tarPath: filePath,
filePath: manifestLocation,
parseJson: true,
});
if (!manifest.main && !manifest.renderer) {
throw new Error(
`${manifestFilename} must specify "main" and/or "renderer" fields`,
);
if (
isObject(manifest)
&& (
hasTypedProperty(manifest, "main", isString)
|| hasTypedProperty(manifest, "renderer", isString)
)
) {
return manifest as unknown as LensExtensionManifest;
}
return manifest;
throw new Error(`${manifestFilename} must specify "main" and/or "renderer" fields`);
};
@@ -30,7 +30,9 @@ const confirmUninstallExtension = ({
const confirmed = await confirm({
message: (
<p>
Are you sure you want to uninstall extension <b>{displayName}</b>?
{"Are you sure you want to uninstall extension "}
<b>{displayName}</b>
?
</p>
),
labelOk: "Yes",
@@ -35,13 +35,14 @@ import installOnDropInjectable from "./install-on-drop/install-on-drop.injectabl
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: (input: string) => Promise<void>;
installFromInput: InstallFromInput;
installFromSelectFileDialog: () => Promise<void>;
installOnDrop: (files: File[]) => Promise<void>;
extensionInstallationStateStore: ExtensionInstallationStateStore;
@@ -73,15 +74,33 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
return (
<DropFileInput onDropFiles={this.props.installOnDrop}>
<SettingLayout className="Extensions" contentGaps={false} data-testid="extensions-page">
<SettingLayout
className="Extensions"
contentGaps={false}
data-testid="extensions-page"
>
<section>
<h1>Extensions</h1>
<Notice className="mb-14 mt-3">
<p>
Add new features via Lens Extensions.{" "}
Check out <a href={`${docsUrl}/extensions/`} target="_blank" rel="noreferrer">docs</a>{" "}
and list of <a href="https://github.com/lensapp/lens-extensions/blob/main/README.md" target="_blank" rel="noreferrer">available extensions</a>.
{"Add new features via Lens Extensions. Check out the "}
<a
href={`${docsUrl}/extensions/`}
target="_blank"
rel="noreferrer"
>
docs
</a>
{" and list of "}
<a
href="https://github.com/lensapp/lens-extensions/blob/main/README.md"
target="_blank"
rel="noreferrer"
>
available extensions
</a>
.
</p>
</Notice>
@@ -5,7 +5,7 @@
import React from "react";
import type { ExtensionRegistry } from "../../../../common/user-store/preferences-helpers";
import { defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../../common/user-store/preferences-helpers";
import { defaultExtensionRegistryUrl } from "../../../../common/user-store/preferences-helpers";
import { promiseExecFile } from "../../../utils";
import { Notifications } from "../../notifications";
@@ -17,10 +17,10 @@ export const getBaseRegistryUrl = ({ getRegistryUrlPreference }: Dependencies) =
const extensionRegistryUrl = getRegistryUrlPreference();
switch (extensionRegistryUrl.location) {
case ExtensionRegistryLocation.CUSTOM:
case "custom":
return extensionRegistryUrl.customUrl;
case ExtensionRegistryLocation.NPMRC: {
case "npmrc": {
try {
const filteredEnv = Object.fromEntries(
Object.entries(process.env)
@@ -30,13 +30,20 @@ export const getBaseRegistryUrl = ({ getRegistryUrlPreference }: Dependencies) =
return stdout.trim();
} catch (error) {
Notifications.error(<p>Failed to get configured registry from <code>.npmrc</code>. Falling back to default registry</p>);
Notifications.error((
<p>
Failed to get configured registry from
<code>.npmrc</code>
. Falling back to default registry.
</p>
));
console.warn("[EXTENSIONS]: failed to get configured registry from .npmrc", error);
}
// fallthrough
}
// fallthrough
default:
case ExtensionRegistryLocation.DEFAULT:
case "default":
return defaultExtensionRegistryUrl;
}
};
@@ -2,16 +2,19 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export function getMessageFromError(error: any): string {
import { hasTypedProperty, isDefined } from "../../../utils";
export function getMessageFromError(error: unknown): string {
if (!error || typeof error !== "object") {
return "an error has occurred";
}
if (error.message) {
return String(error.message);
if (error instanceof Error) {
return error.message;
}
if (error.err) {
if (hasTypedProperty(error, "err", isDefined)) {
return String(error.err);
}
@@ -14,6 +14,9 @@ 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";
import { AsyncInputValidationError } from "../../input/input_validators";
export type InstallFromInput = (input: string) => Promise<void>;
interface Dependencies {
attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => Promise<void>;
@@ -21,34 +24,59 @@ interface Dependencies {
extensionInstallationStateStore: ExtensionInstallationStateStore;
}
export const installFromInput = ({ attemptInstall, attemptInstallByInfo, extensionInstallationStateStore }: Dependencies) => async (input: string) => {
let disposer: ExtendableDisposer | undefined = undefined;
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);
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);
await attemptInstall({ fileName, dataP: promise }, disposer);
} else if (InputValidators.isPath.validate(input)) {
// install from system path
const fileName = path.basename(input);
return await attemptInstall({ fileName, dataP: promise }, disposer);
}
await attemptInstall({ fileName, dataP: readFileNotify(input) });
} else if (InputValidators.isExtensionNameInstall.validate(input)) {
const [{ groups: { name, version }}] = [...input.matchAll(InputValidators.isExtensionNameInstallRegex)];
try {
await InputValidators.isPath.validate(input, {});
await attemptInstallByInfo({ name, version });
// install from system path
const fileName = path.basename(input);
return await attemptInstall({ fileName, dataP: readFileNotify(input) });
} catch (error) {
if (error instanceof AsyncInputValidationError) {
const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input);
if (extNameCaptures) {
const { name, version } = extNameCaptures;
return await attemptInstallByInfo({ name, version });
}
} else {
throw error;
}
}
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?.();
}
} 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?.();
}
};
);
@@ -9,7 +9,6 @@ import { prevDefault } from "../../utils";
import { Button } from "../button";
import { Icon } from "../icon";
import { observer } from "mobx-react";
import type { InputValidator } from "../input";
import { Input, InputValidators } from "../input";
import { SubTitle } from "../layout/sub-title";
import { TooltipPosition } from "../tooltip";
@@ -17,6 +16,7 @@ import type { ExtensionInstallationStateStore } from "../../../extensions/extens
import extensionInstallationStateStoreInjectable
from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import { inputValidator } from "../input/input_validators";
export interface InstallProps {
installPath: string;
@@ -36,12 +36,12 @@ const installInputValidators = [
InputValidators.isExtensionNameInstall,
];
const installInputValidator: InputValidator = {
const installInputValidator = inputValidator({
message: "Invalid URL, absolute path, or extension name",
validate: (value: string) => (
installInputValidators.some(({ validate }) => validate(value))
validate: (value: string, props) => (
installInputValidators.some(({ validate }) => validate(value, props))
),
};
});
const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({
installPath,
@@ -71,14 +71,14 @@ const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({
value={installPath}
onChange={onChange}
onSubmit={installFromInput}
iconRight={
iconRight={(
<Icon
className={styles.icon}
material="folder_open"
onClick={prevDefault(installFromSelectFileDialog)}
tooltip="Browse"
/>
}
)}
/>
</div>
<div className="flex-initial">
@@ -95,7 +95,8 @@ const NonInjectedInstall: React.FC<Dependencies & InstallProps> = ({
</div>
</div>
<small className="mt-3">
<b>Pro-Tip</b>: you can drag-n-drop tarball-file to this area
<b>Pro-Tip</b>
: you can drag-n-drop tarball-file to this area
</small>
</section>
);
@@ -46,12 +46,6 @@ function getStatus(extension: InstalledExtension) {
}
const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extensionInstallationStateStore, extensions, uninstall, enable, disable }: Dependencies & InstalledExtensionsProps) => {
const filters = [
(extension: InstalledExtension) => extension.manifest.name,
(extension: InstalledExtension) => getStatus(extension),
(extension: InstalledExtension) => extension.manifest.version,
];
const columns = useMemo(
() => [
{
@@ -87,63 +81,61 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension
);
const data = useMemo(
() => {
return extensions.map(extension => {
const { id, isEnabled, isCompatible, manifest } = extension;
const { name, description, version } = manifest;
const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id);
() => extensions.map(extension => {
const { id, isEnabled, isCompatible, manifest } = extension;
const { name, description, version } = manifest;
const isUninstalling = extensionInstallationStateStore.isExtensionUninstalling(id);
return {
extension: (
<div className={"flex items-start"}>
<div>
<div className={styles.extensionName}>{name}</div>
<div className={styles.extensionDescription}>{description}</div>
</div>
return {
extension: (
<div className={"flex items-start"}>
<div>
<div className={styles.extensionName}>{name}</div>
<div className={styles.extensionDescription}>{description}</div>
</div>
),
version,
status: (
<div className={cssNames({ [styles.enabled]: isEnabled, [styles.invalid]: !isCompatible })}>
{getStatus(extension)}
</div>
),
actions: (
<MenuActions usePortal toolbar={false}>
{ isCompatible && (
<>
{isEnabled ? (
<MenuItem
disabled={isUninstalling}
onClick={() => disable(id)}
>
<Icon material="unpublished"/>
<span className="title" aria-disabled={isUninstalling}>Disable</span>
</MenuItem>
) : (
<MenuItem
disabled={isUninstalling}
onClick={() => enable(id)}
>
<Icon material="check_circle"/>
<span className="title" aria-disabled={isUninstalling}>Enable</span>
</MenuItem>
)}
</>
)}
</div>
),
version,
status: (
<div className={cssNames({ [styles.enabled]: isEnabled, [styles.invalid]: !isCompatible })}>
{getStatus(extension)}
</div>
),
actions: (
<MenuActions usePortal toolbar={false}>
{isCompatible && (
<>
{isEnabled ? (
<MenuItem
disabled={isUninstalling}
onClick={() => disable(id)}
>
<Icon material="unpublished" />
<span className="title" aria-disabled={isUninstalling}>Disable</span>
</MenuItem>
) : (
<MenuItem
disabled={isUninstalling}
onClick={() => enable(id)}
>
<Icon material="check_circle" />
<span className="title" aria-disabled={isUninstalling}>Enable</span>
</MenuItem>
)}
</>
)}
<MenuItem
disabled={isUninstalling}
onClick={() => uninstall(extension)}
>
<Icon material="delete"/>
<span className="title" aria-disabled={isUninstalling}>Uninstall</span>
</MenuItem>
</MenuActions>
),
};
});
}, [extensions, extensionInstallationStateStore.anyUninstalling],
<MenuItem
disabled={isUninstalling}
onClick={() => uninstall(extension)}
>
<Icon material="delete" />
<span className="title" aria-disabled={isUninstalling}>Uninstall</span>
</MenuItem>
</MenuActions>
),
};
}), [extensions, extensionInstallationStateStore.anyUninstalling],
);
if (!extensionDiscovery.isLoaded) {
@@ -169,7 +161,11 @@ const NonInjectedInstalledExtensions = observer(({ extensionDiscovery, extension
columns={columns}
data={data}
items={extensions}
filters={filters}
filters={[
(extension) => extension.manifest.name,
(extension) => getStatus(extension),
(extension) => extension.manifest.version,
]}
/>
</section>
);
@@ -19,45 +19,61 @@ interface Dependencies {
extensionInstallationStateStore: ExtensionInstallationStateStore;
}
export const uninstallExtension =
({ extensionLoader, extensionDiscovery, extensionInstallationStateStore }: Dependencies) =>
async (extensionId: LensExtensionId): Promise<boolean> => {
const { manifest } = extensionLoader.getExtension(extensionId);
const displayName = extensionDisplayName(manifest.name, manifest.version);
export const uninstallExtension = ({
extensionLoader,
extensionDiscovery,
extensionInstallationStateStore,
}: Dependencies) => (
async (extensionId: LensExtensionId): Promise<boolean> => {
const ext = extensionLoader.getExtension(extensionId);
try {
logger.debug(`[EXTENSIONS]: trying to uninstall ${extensionId}`);
extensionInstallationStateStore.setUninstalling(extensionId);
if (!ext) {
logger.debug(`[EXTENSIONS]: cannot uninstall ${extensionId}, was not installed`);
await extensionDiscovery.uninstallExtension(extensionId);
return true;
}
// wait for the ExtensionLoader to actually uninstall the extension
await when(() => !extensionLoader.userExtensions.has(extensionId));
const { manifest } = ext;
const displayName = extensionDisplayName(manifest.name, manifest.version);
Notifications.ok(
<p>
Extension <b>{displayName}</b> successfully uninstalled!
</p>,
);
try {
logger.debug(`[EXTENSIONS]: trying to uninstall ${extensionId}`);
extensionInstallationStateStore.setUninstalling(extensionId);
return true;
} catch (error) {
const message = getMessageFromError(error);
await extensionDiscovery.uninstallExtension(extensionId);
logger.info(
`[EXTENSION-UNINSTALL]: uninstalling ${displayName} has failed: ${error}`,
{ error },
);
Notifications.error(
<p>
Uninstalling extension <b>{displayName}</b> has failed:{" "}
<em>{message}</em>
</p>,
);
// wait for the ExtensionLoader to actually uninstall the extension
await when(() => !extensionLoader.userExtensions.has(extensionId));
return false;
} finally {
Notifications.ok(
<p>
{"Extension "}
<b>{displayName}</b>
{" successfully uninstalled!"}
</p>,
);
return true;
} catch (error) {
const message = getMessageFromError(error);
logger.info(
`[EXTENSION-UNINSTALL]: uninstalling ${displayName} has failed: ${error}`,
{ error },
);
Notifications.error(
<p>
{"Uninstalling extension "}
<b>{displayName}</b>
{" has failed: "}
<em>{message}</em>
</p>,
);
return false;
} finally {
// Remove uninstall state on uninstall failure
extensionInstallationStateStore.clearUninstalling(extensionId);
}
};
extensionInstallationStateStore.clearUninstalling(extensionId);
}
}
);