mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into wrap-pod-logs
This commit is contained in:
commit
31c7ae118d
1328 files changed
+73244
-43415
No files matched your search
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { Response } from "node-fetch";
|
||||
import type { JsonApiErrorParsed } from "../../common/k8s-api/json-api";
|
||||
import { Notifications } from "../components/notifications";
|
||||
|
||||
export function onApiError(error: JsonApiErrorParsed, res: Response) {
|
||||
switch (res.status) {
|
||||
case 403:
|
||||
error.isUsedForNotification = true;
|
||||
Notifications.error(error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { apiBase } from "../../common/k8s-api";
|
||||
import { onApiError } from "./on-api-error";
|
||||
import { beforeFrameStartsInjectionToken } from "../before-frame-starts/before-frame-starts-injection-token";
|
||||
|
||||
const setupOnApiErrorListenersInjectable = getInjectable({
|
||||
id: "setup-on-api-error-listeners",
|
||||
|
||||
instantiate: () => ({
|
||||
run: () => {
|
||||
apiBase?.onError.addListener(onApiError);
|
||||
},
|
||||
}),
|
||||
|
||||
injectionToken: beforeFrameStartsInjectionToken,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default setupOnApiErrorListenersInjectable;
|
||||
@@ -5,7 +5,7 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import appPathsStateInjectable from "../../common/app-paths/app-paths-state.injectable";
|
||||
import { beforeFrameStartsInjectionToken } from "../before-frame-starts/before-frame-starts-injection-token";
|
||||
import appPathsChannelInjectable from "../../common/app-paths/app-paths-channel.injectable";
|
||||
import { appPathsChannel } from "../../common/app-paths/app-paths-channel";
|
||||
import { requestFromChannelInjectionToken } from "../../common/utils/channel/request-from-channel-injection-token";
|
||||
|
||||
const setupAppPathsInjectable = getInjectable({
|
||||
@@ -13,14 +13,12 @@ const setupAppPathsInjectable = getInjectable({
|
||||
|
||||
instantiate: (di) => {
|
||||
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
|
||||
const appPathsChannel = di.inject(appPathsChannelInjectable);
|
||||
const appPathsState = di.inject(appPathsStateInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-app-paths",
|
||||
run: async () => {
|
||||
const appPaths = await requestFromChannel(
|
||||
appPathsChannel,
|
||||
);
|
||||
const appPaths = await requestFromChannel(appPathsChannel);
|
||||
|
||||
appPathsState.set(appPaths);
|
||||
},
|
||||
|
||||
-39
@@ -1,39 +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 { computed } from "mobx";
|
||||
import { rootFrameChildComponentInjectionToken } from "../../frames/root-frame/root-frame-child-component-injection-token";
|
||||
import { ForceUpdateModal } from "./force-update-modal";
|
||||
import timeSinceUpdateWasDownloadedInjectable from "./time-since-update-was-downloaded.injectable";
|
||||
import updateDownloadedDateTimeInjectable from "../../../common/application-update/update-downloaded-date-time/update-downloaded-date-time.injectable";
|
||||
import timeAfterUpdateMustBeInstalledInjectable from "./time-after-update-must-be-installed.injectable";
|
||||
|
||||
const forceUpdateModalRootFrameComponentInjectable = getInjectable({
|
||||
id: "force-update-modal-root-frame-component",
|
||||
|
||||
instantiate: (di) => {
|
||||
const timeSinceUpdateWasDownloaded = di.inject(timeSinceUpdateWasDownloadedInjectable);
|
||||
const updateDownloadedDateTime = di.inject(updateDownloadedDateTimeInjectable);
|
||||
const timeWhenUpdateMustBeInstalled = di.inject(timeAfterUpdateMustBeInstalledInjectable);
|
||||
|
||||
return {
|
||||
id: "force-update-modal",
|
||||
Component: ForceUpdateModal,
|
||||
|
||||
shouldRender: computed(
|
||||
() =>
|
||||
!!updateDownloadedDateTime.value.get() &&
|
||||
timeSinceUpdateWasDownloaded.get() >= timeWhenUpdateMustBeInstalled,
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
injectionToken: rootFrameChildComponentInjectionToken,
|
||||
|
||||
// Note: Globally overridden because it observes the current time which can cause long running tests
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default forceUpdateModalRootFrameComponentInjectable;
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
$baseline: 8px;
|
||||
|
||||
.ForceUpdateModal {
|
||||
background-color: white;
|
||||
border-radius: 1 * $baseline;
|
||||
padding: 3 * $baseline;
|
||||
width: 50 * $baseline;
|
||||
|
||||
.header {
|
||||
margin-bottom: 2 * $baseline;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-bottom: 2 * $baseline;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import React from "react";
|
||||
import restartAndInstallUpdateInjectable from "../../components/update-button/restart-and-install-update.injectable";
|
||||
import { Countdown } from "../../components/countdown/countdown";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import installUpdateCountdownInjectable from "./install-update-countdown.injectable";
|
||||
import { Dialog } from "../../components/dialog";
|
||||
import { Button } from "../../components/button";
|
||||
import styles from "./force-update-modal.module.scss";
|
||||
|
||||
interface Dependencies {
|
||||
restartAndInstallUpdate: () => void;
|
||||
secondsTill: IComputedValue<number>;
|
||||
}
|
||||
|
||||
const NonInjectedForceUpdateModal = observer(
|
||||
({ restartAndInstallUpdate, secondsTill }: Dependencies) => (
|
||||
<Dialog isOpen={true} pinned>
|
||||
<div
|
||||
data-testid="must-update-immediately"
|
||||
className={styles.ForceUpdateModal}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<h2>Please update</h2>
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
<p>
|
||||
An update to Lens Desktop is required to continue using the application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
primary
|
||||
data-testid="update-now-from-must-update-immediately-modal"
|
||||
onClick={restartAndInstallUpdate}
|
||||
label="Update"
|
||||
>
|
||||
{" "}
|
||||
(
|
||||
<Countdown
|
||||
secondsTill={secondsTill}
|
||||
data-testid="countdown-to-automatic-update"
|
||||
/>
|
||||
)
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
),
|
||||
);
|
||||
|
||||
export const ForceUpdateModal = withInjectables<Dependencies>(
|
||||
NonInjectedForceUpdateModal,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
restartAndInstallUpdate: di.inject(restartAndInstallUpdateInjectable),
|
||||
secondsTill: di.inject(installUpdateCountdownInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
-27
@@ -1,27 +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 countdownStateInjectable from "../../components/countdown/countdown-state.injectable";
|
||||
import secondsAfterInstallStartsInjectable from "./seconds-after-install-starts.injectable";
|
||||
import restartAndInstallUpdateInjectable from "../../components/update-button/restart-and-install-update.injectable";
|
||||
|
||||
const installUpdateCountdownInjectable = getInjectable({
|
||||
id: "install-update-countdown",
|
||||
|
||||
instantiate: (di) => {
|
||||
const secondsAfterInstallStarts = di.inject(secondsAfterInstallStartsInjectable);
|
||||
const restartAndInstallUpdate = di.inject(restartAndInstallUpdateInjectable);
|
||||
|
||||
return di.inject(countdownStateInjectable, {
|
||||
startFrom: secondsAfterInstallStarts,
|
||||
|
||||
onZero: () => {
|
||||
restartAndInstallUpdate();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default installUpdateCountdownInjectable;
|
||||
-14
@@ -1,14 +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";
|
||||
|
||||
const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const timeAfterUpdateMustBeInstalledInjectable = getInjectable({
|
||||
id: "time-after-update-must-be-installed",
|
||||
instantiate: () => THIRTY_DAYS,
|
||||
});
|
||||
|
||||
export default timeAfterUpdateMustBeInstalledInjectable;
|
||||
-30
@@ -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 assert from "assert";
|
||||
import { computed } from "mobx";
|
||||
import moment from "moment";
|
||||
import updateDownloadedDateTimeInjectable from "../../../common/application-update/update-downloaded-date-time/update-downloaded-date-time.injectable";
|
||||
import { reactiveNow } from "../../../common/utils/reactive-now/reactive-now";
|
||||
|
||||
const timeSinceUpdateWasDownloadedInjectable = getInjectable({
|
||||
id: "time-since-update-was-downloaded",
|
||||
|
||||
instantiate: (di) => {
|
||||
const updateDownloadedDateTime = di.inject(updateDownloadedDateTimeInjectable);
|
||||
|
||||
return computed(() => {
|
||||
const currentTimestamp = reactiveNow();
|
||||
|
||||
const downloadedAt = updateDownloadedDateTime.value.get();
|
||||
|
||||
assert(downloadedAt);
|
||||
|
||||
return currentTimestamp - (moment(downloadedAt).unix() * 1000);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default timeSinceUpdateWasDownloadedInjectable;
|
||||
@@ -20,7 +20,6 @@ import configurePackages from "../common/configure-packages";
|
||||
import * as initializers from "./initializers";
|
||||
import logger from "../common/logger";
|
||||
import { WeblinkStore } from "../common/weblink-store";
|
||||
import { initializeSentryReporting } from "../common/sentry";
|
||||
import { registerCustomThemes } from "./components/monaco-editor";
|
||||
import { getDi } from "./getDi";
|
||||
import { DiContextProvider } from "@ogre-tools/injectable-react";
|
||||
@@ -29,7 +28,6 @@ import extensionLoaderInjectable from "../extensions/extension-loader/extension-
|
||||
import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
|
||||
import userStoreInjectable from "../common/user-store/user-store.injectable";
|
||||
import initRootFrameInjectable from "./frames/root-frame/init-root-frame/init-root-frame.injectable";
|
||||
import initClusterFrameInjectable from "./frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable";
|
||||
import commandOverlayInjectable from "./components/command-palette/command-overlay.injectable";
|
||||
@@ -46,6 +44,7 @@ import kubernetesClusterCategoryInjectable from "../common/catalog/categories/ku
|
||||
import autoRegistrationInjectable from "../common/k8s-api/api-manager/auto-registration.injectable";
|
||||
import assert from "assert";
|
||||
import startFrameInjectable from "./start-frame/start-frame.injectable";
|
||||
import initializeSentryReportingWithInjectable from "../common/error-reporting/initialize-sentry-reporting.injectable";
|
||||
|
||||
configurePackages(); // global packages
|
||||
registerCustomThemes(); // monaco editor themes
|
||||
@@ -62,8 +61,10 @@ async function attachChromeDebugger() {
|
||||
}
|
||||
|
||||
export async function bootstrap(di: DiContainer) {
|
||||
const initializeSentryReportingWith = di.inject(initializeSentryReportingWithInjectable);
|
||||
|
||||
if (process.isMainFrame) {
|
||||
initializeSentryReporting(init);
|
||||
initializeSentryReportingWith(init);
|
||||
}
|
||||
|
||||
const startFrame = di.inject(startFrameInjectable);
|
||||
@@ -90,9 +91,6 @@ export async function bootstrap(di: DiContainer) {
|
||||
*/
|
||||
di.inject(autoRegistrationInjectable);
|
||||
|
||||
// TODO: Remove temporal dependencies to make timing of initialization not important
|
||||
di.inject(userStoreInjectable);
|
||||
|
||||
await attachChromeDebugger();
|
||||
rootElem.classList.toggle("is-mac", isMac);
|
||||
|
||||
|
||||
@@ -10,10 +10,8 @@ import fse from "fs-extra";
|
||||
import { debounce } from "lodash";
|
||||
import { action, computed, makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import path from "path";
|
||||
import React from "react";
|
||||
import * as uuid from "uuid";
|
||||
import { appEventBus } from "../../../common/app-event-bus/event-bus";
|
||||
import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers";
|
||||
import { docsUrl } from "../../../common/vars";
|
||||
import { isDefined, iter } from "../../utils";
|
||||
@@ -25,6 +23,10 @@ import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import getCustomKubeConfigDirectoryInjectable from "../../../common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
|
||||
import type { NavigateToCatalog } from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
|
||||
import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
|
||||
import type { EmitAppEvent } from "../../../common/app-event-bus/emit-event.injectable";
|
||||
import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable";
|
||||
import type { GetDirnameOfPath } from "../../../common/path/get-dirname.injectable";
|
||||
import getDirnameOfPathInjectable from "../../../common/path/get-dirname.injectable";
|
||||
|
||||
interface Option {
|
||||
config: KubeConfig;
|
||||
@@ -34,6 +36,8 @@ interface Option {
|
||||
interface Dependencies {
|
||||
getCustomKubeConfigDirectory: (directoryName: string) => string;
|
||||
navigateToCatalog: NavigateToCatalog;
|
||||
getDirnameOfPath: GetDirnameOfPath;
|
||||
emitAppEvent: EmitAppEvent;
|
||||
}
|
||||
|
||||
function getContexts(config: KubeConfig): Map<string, Option> {
|
||||
@@ -53,13 +57,13 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
|
||||
@observable isWaiting = false;
|
||||
@observable errors: string[] = [];
|
||||
|
||||
constructor(dependencies: Dependencies) {
|
||||
super(dependencies);
|
||||
constructor(props: Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
appEventBus.emit({ name: "cluster-add", action: "start" });
|
||||
this.props.emitAppEvent({ name: "cluster-add", action: "start" });
|
||||
}
|
||||
|
||||
@computed get allErrors(): string[] {
|
||||
@@ -85,12 +89,12 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
|
||||
|
||||
addClusters = action(async () => {
|
||||
this.isWaiting = true;
|
||||
appEventBus.emit({ name: "cluster-add", action: "click" });
|
||||
this.props.emitAppEvent({ name: "cluster-add", action: "click" });
|
||||
|
||||
try {
|
||||
const absPath = this.props.getCustomKubeConfigDirectory(uuid.v4());
|
||||
|
||||
await fse.ensureDir(path.dirname(absPath));
|
||||
await fse.ensureDir(this.props.getDirnameOfPath(absPath));
|
||||
await fse.writeFile(absPath, this.customConfig.trim(), { encoding: "utf-8", mode: 0o600 });
|
||||
|
||||
Notifications.ok(`Successfully added ${this.kubeContexts.size} new cluster(s)`);
|
||||
@@ -112,7 +116,7 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
|
||||
<code>~/.kube/config</code>
|
||||
{" file. "}
|
||||
<a
|
||||
href={`${docsUrl}/catalog/add-cluster/`}
|
||||
href={`${docsUrl}/getting-started/add-cluster/`}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
@@ -155,10 +159,9 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
|
||||
|
||||
export const AddCluster = withInjectables<Dependencies>(NonInjectedAddCluster, {
|
||||
getProps: (di) => ({
|
||||
getCustomKubeConfigDirectory: di.inject(
|
||||
getCustomKubeConfigDirectoryInjectable,
|
||||
),
|
||||
|
||||
getCustomKubeConfigDirectory: di.inject(getCustomKubeConfigDirectoryInjectable),
|
||||
navigateToCatalog: di.inject(navigateToCatalogInjectable),
|
||||
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
|
||||
emitAppEvent: di.inject(emitAppEventInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -3,11 +3,13 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import type { CatalogCategorySpec } from "../../../../common/catalog";
|
||||
import { CatalogCategory } from "../../../../common/catalog";
|
||||
import { CatalogAddButton } from "../catalog-add-button";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import { type DiRender, renderFor } from "../../test-utils/renderFor";
|
||||
|
||||
class TestCatalogCategory extends CatalogCategory {
|
||||
public readonly apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
@@ -26,6 +28,14 @@ class TestCatalogCategory extends CatalogCategory {
|
||||
}
|
||||
|
||||
describe("CatalogAddButton", () => {
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
render = renderFor(di);
|
||||
});
|
||||
|
||||
it("opens Add menu", async () => {
|
||||
const category = new TestCatalogCategory();
|
||||
|
||||
|
||||
@@ -4,16 +4,15 @@
|
||||
*/
|
||||
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import { computed } from "mobx";
|
||||
import type { CatalogCategorySpec } from "../../../../common/catalog";
|
||||
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
|
||||
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
|
||||
import { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
|
||||
import { CatalogCategory } from "../../../api/catalog-entity";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import type { AdditionalCategoryColumnRegistration, CategoryColumnRegistration } from "../custom-category-columns";
|
||||
import type { CategoryColumns, GetCategoryColumnsParams } from "../columns/get.injectable";
|
||||
import getCategoryColumnsInjectable from "../columns/get.injectable";
|
||||
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
|
||||
import extensionInjectable from "../../../../extensions/extension-loader/extension/extension.injectable";
|
||||
|
||||
class TestCategory extends CatalogCategory {
|
||||
apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
@@ -41,21 +40,16 @@ class TestCategory extends CatalogCategory {
|
||||
|
||||
describe("Custom Category Columns", () => {
|
||||
let di: DiContainer;
|
||||
let getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
|
||||
|
||||
beforeEach(() => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(hotbarStoreInjectable, () => ({}));
|
||||
getCategoryColumns = di.inject(getCategoryColumnsInjectable);
|
||||
});
|
||||
|
||||
describe("without extensions", () => {
|
||||
let getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
|
||||
|
||||
beforeEach(() => {
|
||||
di.override(rendererExtensionsInjectable, () => computed(() => [] as LensRendererExtension[]));
|
||||
getCategoryColumns = di.inject(getCategoryColumnsInjectable);
|
||||
});
|
||||
|
||||
it("should contain a kind column if activeCategory is falsy", () => {
|
||||
expect(getCategoryColumns({ activeCategory: null }).renderTableHeader.find(elem => elem?.title === "Kind")).toBeTruthy();
|
||||
});
|
||||
@@ -88,13 +82,9 @@ describe("Custom Category Columns", () => {
|
||||
});
|
||||
|
||||
describe("with extensions", () => {
|
||||
let getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
|
||||
|
||||
beforeEach(() => {
|
||||
di.override(rendererExtensionsInjectable, () => computed(() => [
|
||||
{
|
||||
name: "test-extension",
|
||||
additionalCategoryColumns: [
|
||||
const ext = di.inject(extensionInjectable, new (class extends LensRendererExtension {
|
||||
additionalCategoryColumns = [
|
||||
{
|
||||
group: "foo.bar.bat",
|
||||
id: "high",
|
||||
@@ -113,10 +103,24 @@ describe("Custom Category Columns", () => {
|
||||
title: "High2",
|
||||
},
|
||||
} as AdditionalCategoryColumnRegistration,
|
||||
],
|
||||
} as LensRendererExtension,
|
||||
]));
|
||||
getCategoryColumns = di.inject(getCategoryColumnsInjectable);
|
||||
];
|
||||
})({
|
||||
absolutePath: "/some-absolute-path",
|
||||
id: "some-id",
|
||||
isBundled: false,
|
||||
isCompatible: true,
|
||||
isEnabled: true,
|
||||
manifest: {
|
||||
engines: {
|
||||
lens: "",
|
||||
},
|
||||
name: "some-extension-name",
|
||||
version: "1.0.0",
|
||||
},
|
||||
manifestPath: "/some-manifest-path",
|
||||
}));
|
||||
|
||||
ext.register();
|
||||
});
|
||||
|
||||
it("should include columns from extensions that match", () => {
|
||||
|
||||
@@ -9,23 +9,21 @@ import styles from "./catalog-menu.module.scss";
|
||||
import React from "react";
|
||||
import type { TreeItemProps } from "@material-ui/lab";
|
||||
import { TreeItem, TreeView } from "@material-ui/lab";
|
||||
import { catalogCategoryRegistry } from "../../api/catalog-category-registry";
|
||||
import { Icon } from "../icon";
|
||||
import { StylesProvider } from "@material-ui/core";
|
||||
import { cssNames } from "../../utils";
|
||||
import type { CatalogCategory } from "../../api/catalog-entity";
|
||||
import { observer } from "mobx-react";
|
||||
import { CatalogCategoryLabel } from "./catalog-category-label";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import filteredCategoriesInjectable from "../../../common/catalog/filtered-categories.injectable";
|
||||
|
||||
export interface CatalogMenuProps {
|
||||
activeTab: string | undefined;
|
||||
onItemClick: (id: string) => void;
|
||||
}
|
||||
|
||||
function getCategories() {
|
||||
return catalogCategoryRegistry.filteredItems;
|
||||
}
|
||||
|
||||
function getCategoryIcon(category: CatalogCategory) {
|
||||
const { icon } = category.metadata ?? {};
|
||||
|
||||
@@ -44,44 +42,56 @@ function Item(props: TreeItemProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export const CatalogMenu = observer((props: CatalogMenuProps) => {
|
||||
return (
|
||||
// Overwrite Material UI styles with injectFirst https://material-ui.com/guides/interoperability/#controlling-priority-4
|
||||
<StylesProvider injectFirst>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className={styles.catalog}>Catalog</div>
|
||||
<TreeView
|
||||
defaultExpanded={["catalog"]}
|
||||
defaultCollapseIcon={<Icon material="expand_more"/>}
|
||||
defaultExpandIcon={<Icon material="chevron_right" />}
|
||||
selected={props.activeTab || "browse"}
|
||||
interface Dependencies {
|
||||
filteredCategories: IComputedValue<CatalogCategory[]>;
|
||||
}
|
||||
|
||||
const NonInjectedCatalogMenu = observer(({
|
||||
activeTab,
|
||||
filteredCategories,
|
||||
onItemClick,
|
||||
}: CatalogMenuProps & Dependencies) => (
|
||||
// Overwrite Material UI styles with injectFirst https://material-ui.com/guides/interoperability/#controlling-priority-4
|
||||
<StylesProvider injectFirst>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className={styles.catalog}>Catalog</div>
|
||||
<TreeView
|
||||
defaultExpanded={["catalog"]}
|
||||
defaultCollapseIcon={<Icon material="expand_more" />}
|
||||
defaultExpandIcon={<Icon material="chevron_right" />}
|
||||
selected={activeTab || "browse"}
|
||||
>
|
||||
<Item
|
||||
nodeId="browse"
|
||||
label="Browse"
|
||||
data-testid="*-tab"
|
||||
onClick={() => onItemClick("*")} />
|
||||
<Item
|
||||
nodeId="catalog"
|
||||
label={<div className={styles.parent}>Categories</div>}
|
||||
className={cssNames(styles.bordered)}
|
||||
>
|
||||
<Item
|
||||
nodeId="browse"
|
||||
label="Browse"
|
||||
data-testid="*-tab"
|
||||
onClick={() => props.onItemClick("*")}
|
||||
/>
|
||||
<Item
|
||||
nodeId="catalog"
|
||||
label={<div className={styles.parent}>Categories</div>}
|
||||
className={cssNames(styles.bordered)}
|
||||
>
|
||||
{
|
||||
getCategories().map(category => (
|
||||
{
|
||||
filteredCategories.get()
|
||||
.map(category => (
|
||||
<Item
|
||||
icon={getCategoryIcon(category)}
|
||||
key={category.getId()}
|
||||
nodeId={category.getId()}
|
||||
label={<CatalogCategoryLabel category={category}/>}
|
||||
label={<CatalogCategoryLabel category={category} />}
|
||||
data-testid={`${category.getId()}-tab`}
|
||||
onClick={() => props.onItemClick(category.getId())}
|
||||
/>
|
||||
onClick={() => onItemClick(category.getId())} />
|
||||
))
|
||||
}
|
||||
</Item>
|
||||
</TreeView>
|
||||
</div>
|
||||
</StylesProvider>
|
||||
);
|
||||
}
|
||||
</Item>
|
||||
</TreeView>
|
||||
</div>
|
||||
</StylesProvider>
|
||||
));
|
||||
|
||||
export const CatalogMenu = withInjectables<Dependencies, CatalogMenuProps>(NonInjectedCatalogMenu, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
filteredCategories: di.inject(filteredCategoriesInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -57,7 +57,7 @@ describe("<Catalog />", () => {
|
||||
let di: DiContainer;
|
||||
let catalogEntityStore: CatalogEntityStore;
|
||||
let catalogEntityRegistry: CatalogEntityRegistry;
|
||||
let emitEvent: (event: AppEvent) => void;
|
||||
let appEventListener: jest.MockedFunction<(event: AppEvent) => void>;
|
||||
let onRun: jest.MockedFunction<(context: CatalogEntityActionContext) => void | Promise<void>>;
|
||||
let catalogEntityItem: MockCatalogEntity;
|
||||
let render: DiRender;
|
||||
@@ -78,11 +78,8 @@ describe("<Catalog />", () => {
|
||||
catalogEntityItem = createMockCatalogEntity(onRun);
|
||||
catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
|
||||
|
||||
emitEvent = jest.fn();
|
||||
|
||||
di.override(appEventBusInjectable, () => ({
|
||||
emit: emitEvent,
|
||||
}));
|
||||
appEventListener = jest.fn();
|
||||
di.inject(appEventBusInjectable).addListener(appEventListener);
|
||||
|
||||
catalogEntityStore = di.inject(catalogEntityStoreInjectable);
|
||||
Object.assign(catalogEntityStore, {
|
||||
@@ -204,24 +201,20 @@ describe("<Catalog />", () => {
|
||||
});
|
||||
|
||||
it("emits catalog open AppEvent", () => {
|
||||
render(
|
||||
<Catalog />,
|
||||
);
|
||||
render(<Catalog />);
|
||||
|
||||
expect(emitEvent).toHaveBeenCalledWith( {
|
||||
expect(appEventListener).toHaveBeenCalledWith( {
|
||||
action: "open",
|
||||
name: "catalog",
|
||||
});
|
||||
});
|
||||
|
||||
it("emits catalog change AppEvent when changing the category", () => {
|
||||
render(
|
||||
<Catalog />,
|
||||
);
|
||||
render(<Catalog />);
|
||||
|
||||
userEvent.click(screen.getByText("Web Links"));
|
||||
|
||||
expect(emitEvent).toHaveBeenLastCalledWith({
|
||||
expect(appEventListener).toHaveBeenCalledWith({
|
||||
action: "change-category",
|
||||
name: "catalog",
|
||||
params: {
|
||||
|
||||
@@ -37,8 +37,6 @@ import type { NavigateToCatalog } from "../../../common/front-end-routing/routes
|
||||
import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
|
||||
import catalogRouteParametersInjectable from "./catalog-route-parameters.injectable";
|
||||
import { browseCatalogTab } from "./catalog-browse-tab";
|
||||
import type { AppEvent } from "../../../common/app-event-bus/event-bus";
|
||||
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
|
||||
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbars/store";
|
||||
import type { VisitEntityContextMenu } from "../../../common/catalog/visit-entity-context-menu.injectable";
|
||||
@@ -48,13 +46,15 @@ import type { Navigate } from "../../navigation/navigate.injectable";
|
||||
import navigateInjectable from "../../navigation/navigate.injectable";
|
||||
import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable";
|
||||
import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable";
|
||||
import type { EmitAppEvent } from "../../../common/app-event-bus/emit-event.injectable";
|
||||
import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
catalogPreviousActiveTabStorage: StorageLayer<string | null>;
|
||||
catalogEntityStore: CatalogEntityStore;
|
||||
getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
|
||||
customCategoryViews: IComputedValue<Map<string, Map<string, RegisteredCustomCategoryViewDecl>>>;
|
||||
emitEvent: (event: AppEvent) => void;
|
||||
emitEvent: EmitAppEvent;
|
||||
routeParameters: {
|
||||
group: IComputedValue<string>;
|
||||
kind: IComputedValue<string>;
|
||||
@@ -356,7 +356,7 @@ export const Catalog = withInjectables<Dependencies>(NonInjectedCatalog, {
|
||||
customCategoryViews: di.inject(customCategoryViewsInjectable),
|
||||
routeParameters: di.inject(catalogRouteParametersInjectable),
|
||||
navigateToCatalog: di.inject(navigateToCatalogInjectable),
|
||||
emitEvent: di.inject(appEventBusInjectable).emit,
|
||||
emitEvent: di.inject(emitAppEventInjectable),
|
||||
hotbarStore: di.inject(hotbarStoreInjectable),
|
||||
catalogCategoryRegistry: di.inject(catalogCategoryRegistryInjectable),
|
||||
visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "../custom-category-columns";
|
||||
|
||||
export interface CustomCatalogCategoryRegistration {
|
||||
kind: string;
|
||||
group: string;
|
||||
registration: RegisteredAdditionalCategoryColumn;
|
||||
}
|
||||
|
||||
export const customCatalogCategoryColumnInjectionToken = getInjectionToken<CustomCatalogCategoryRegistration>({
|
||||
id: "custom-catalog-category-column-token",
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 { ExtensionRegistrator } from "../../../../extensions/extension-loader/extension-registrator-injection-token";
|
||||
import { extensionRegistratorInjectionToken } from "../../../../extensions/extension-loader/extension-registrator-injection-token";
|
||||
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
|
||||
import type { AdditionalCategoryColumnRegistration } from "../custom-category-columns";
|
||||
import { customCatalogCategoryColumnInjectionToken } from "./custom-token";
|
||||
|
||||
const customCategoryColumnsRegistratorInjectable = getInjectable({
|
||||
id: "custom-category-columns-registrator",
|
||||
instantiate: (): ExtensionRegistrator => {
|
||||
return (ext) => {
|
||||
const extension = ext as LensRendererExtension;
|
||||
|
||||
return extension.additionalCategoryColumns.map(getInjectableForColumnRegistrationFor(extension));
|
||||
};
|
||||
},
|
||||
injectionToken: extensionRegistratorInjectionToken,
|
||||
});
|
||||
|
||||
export default customCategoryColumnsRegistratorInjectable;
|
||||
|
||||
const getInjectableForColumnRegistrationFor = (extension: LensRendererExtension) => ({
|
||||
group,
|
||||
id,
|
||||
kind,
|
||||
renderCell,
|
||||
titleProps,
|
||||
priority = 50,
|
||||
searchFilter,
|
||||
sortCallback,
|
||||
}: AdditionalCategoryColumnRegistration) => {
|
||||
return getInjectable({
|
||||
id: `${extension.manifest.name}:${group}/${kind}:${id}`,
|
||||
instantiate: () => ({
|
||||
group,
|
||||
kind,
|
||||
registration: {
|
||||
renderCell,
|
||||
priority,
|
||||
id,
|
||||
titleProps: {
|
||||
id,
|
||||
...titleProps,
|
||||
sortBy: sortCallback
|
||||
? id
|
||||
: undefined,
|
||||
},
|
||||
searchFilter,
|
||||
sortCallback,
|
||||
},
|
||||
}),
|
||||
injectionToken: customCatalogCategoryColumnInjectionToken,
|
||||
});
|
||||
};
|
||||
@@ -47,7 +47,10 @@ const getCategoryColumnsInjectable = getInjectable({
|
||||
}
|
||||
|
||||
tableRowRenderers.push(registration.renderCell);
|
||||
renderTableHeader.push(registration.titleProps);
|
||||
renderTableHeader.push({
|
||||
id: registration.id,
|
||||
...registration.titleProps,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
import type { KubernetesCluster } from "../../../../common/catalog-entities";
|
||||
import { customCatalogCategoryColumnInjectionToken } from "./custom-token";
|
||||
|
||||
const kubernetesApiVersionColumnInjectable = getInjectable({
|
||||
id: "kubernetes-api-version-column",
|
||||
instantiate: () => ({
|
||||
group: "entity.k8slens.dev",
|
||||
kind: "KubernetesCluster",
|
||||
registration: {
|
||||
id: "version",
|
||||
priority: 30,
|
||||
renderCell: entity => {
|
||||
const k8sVersion = (entity as KubernetesCluster).metadata.kubeVersion;
|
||||
|
||||
return (
|
||||
<span key="version">
|
||||
{k8sVersion === "unknown" ? "" : k8sVersion}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
titleProps: {
|
||||
title: "Version",
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectionToken: customCatalogCategoryColumnInjectionToken,
|
||||
});
|
||||
|
||||
export default kubernetesApiVersionColumnInjectable;
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
import type { KubernetesCluster } from "../../../../common/catalog-entities";
|
||||
import { customCatalogCategoryColumnInjectionToken } from "./custom-token";
|
||||
|
||||
const kubernetesDistributionColumnInjectable = getInjectable({
|
||||
id: "kubernetes-distribution-column",
|
||||
instantiate: () => ({
|
||||
group: "entity.k8slens.dev",
|
||||
kind: "KubernetesCluster",
|
||||
registration: {
|
||||
id: "distro",
|
||||
priority: 30,
|
||||
renderCell: entity => {
|
||||
const k8sDistro = (entity as KubernetesCluster).metadata.distro;
|
||||
|
||||
return (
|
||||
<span key="distro">
|
||||
{k8sDistro === "unknown" ? "" : k8sDistro}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
titleProps: {
|
||||
title: "Distro",
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectionToken: customCatalogCategoryColumnInjectionToken,
|
||||
});
|
||||
|
||||
export default kubernetesDistributionColumnInjectable;
|
||||
|
||||
@@ -3,54 +3,31 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
|
||||
import { computed } from "mobx";
|
||||
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
|
||||
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
|
||||
import { getOrInsert, getOrInsertMap } from "../../utils";
|
||||
import { customCatalogCategoryColumnInjectionToken } from "./columns/custom-token";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||
|
||||
interface Dependencies {
|
||||
extensions: IComputedValue<LensRendererExtension[]>;
|
||||
}
|
||||
|
||||
function getAdditionCategoryColumns({ extensions }: Dependencies): IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>> {
|
||||
return computed(() => {
|
||||
const res = new Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>();
|
||||
|
||||
for (const ext of extensions.get()) {
|
||||
for (const { renderCell, titleProps, priority = 50, searchFilter, sortCallback, ...registration } of ext.additionalCategoryColumns) {
|
||||
const byGroup = getOrInsertMap(res, registration.group);
|
||||
const byKind = getOrInsert(byGroup, registration.kind, []);
|
||||
const id = `${ext.name}:${registration.id}`;
|
||||
|
||||
byKind.push({
|
||||
renderCell,
|
||||
priority,
|
||||
id,
|
||||
titleProps: {
|
||||
id,
|
||||
...titleProps,
|
||||
sortBy: sortCallback
|
||||
? id
|
||||
: undefined,
|
||||
},
|
||||
searchFilter,
|
||||
sortCallback,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
const categoryColumnsInjectable = getInjectable({
|
||||
id: "category-columns",
|
||||
instantiate: (di) => {
|
||||
const computedInjectMany = di.inject(computedInjectManyInjectable);
|
||||
const columnRegistrations = computedInjectMany(customCatalogCategoryColumnInjectionToken);
|
||||
|
||||
instantiate: (di) => getAdditionCategoryColumns({
|
||||
extensions: di.inject(rendererExtensionsInjectable),
|
||||
}),
|
||||
return computed(() => {
|
||||
const res = new Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>();
|
||||
|
||||
for (const { group, kind, registration } of columnRegistrations.get()) {
|
||||
const byGroup = getOrInsertMap(res, group);
|
||||
const byKind = getOrInsert(byGroup, kind, []);
|
||||
|
||||
byKind.push(registration);
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default categoryColumnsInjectable;
|
||||
@@ -24,7 +24,7 @@ const NonInjectedClusterMetricSwitchers = observer(({
|
||||
nodeStore,
|
||||
}: Dependencies) => {
|
||||
const { masterNodes, workerNodes } = nodeStore;
|
||||
const { cpuUsage, memoryUsage } = clusterOverviewStore.metrics;
|
||||
const { cpuUsage, memoryUsage } = clusterOverviewStore.metrics ?? {};
|
||||
const hasMasterNodes = masterNodes.length > 0;
|
||||
const hasWorkerNodes = workerNodes.length > 0;
|
||||
const hasCpuMetrics = normalizeMetrics(cpuUsage).data.result[0].values.length > 0;
|
||||
|
||||
@@ -26,8 +26,8 @@ interface Dependencies {
|
||||
|
||||
const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics }}: Dependencies) => {
|
||||
const [plugins] = useState([new ZebraStripesPlugin()]);
|
||||
const { memoryCapacity, cpuCapacity } = getMetricLastPoints(metrics);
|
||||
const metricValues = getMetricsValues(metrics);
|
||||
const { memoryCapacity, cpuCapacity } = getMetricLastPoints(metrics ?? {});
|
||||
const metricValues = getMetricsValues(metrics ?? {});
|
||||
const colors = { cpu: "#3D90CE", memory: "#C93DCE" };
|
||||
const data = metricValues.map(value => ({
|
||||
x: value[0],
|
||||
|
||||
+2
@@ -12,6 +12,7 @@ import clusterApiInjectable from "../../../../common/k8s-api/endpoints/cluster.a
|
||||
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable";
|
||||
import assert from "assert";
|
||||
import nodeStoreInjectable from "../../+nodes/store.injectable";
|
||||
import requestClusterMetricsByNodeNamesInjectable from "../../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable";
|
||||
|
||||
const clusterOverviewStoreInjectable = getInjectable({
|
||||
id: "cluster-overview-store",
|
||||
@@ -30,6 +31,7 @@ const clusterOverviewStoreInjectable = getInjectable({
|
||||
},
|
||||
),
|
||||
nodeStore: di.inject(nodeStoreInjectable),
|
||||
requestClusterMetricsByNodeNames: di.inject(requestClusterMetricsByNodeNamesInjectable),
|
||||
}, clusterApi);
|
||||
},
|
||||
injectionToken: kubeObjectStoreInjectionToken,
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
import { action, observable, reaction, when, makeObservable } from "mobx";
|
||||
import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
|
||||
import type { Cluster, ClusterApi } from "../../../../common/k8s-api/endpoints";
|
||||
import { getMetricsByNodeNames, type ClusterMetricData } from "../../../../common/k8s-api/endpoints";
|
||||
import type { StorageLayer } from "../../../utils";
|
||||
import { autoBind } from "../../../utils";
|
||||
import { type IMetricsReqParams, normalizeMetrics } from "../../../../common/k8s-api/endpoints/metrics.api";
|
||||
import type { NodeStore } from "../../+nodes/store";
|
||||
import type { ClusterMetricData, RequestClusterMetricsByNodeNames } from "../../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable";
|
||||
import type { RequestMetricsParams } from "../../../../common/k8s-api/endpoints/metrics.api/request-metrics.injectable";
|
||||
import { normalizeMetrics } from "../../../../common/k8s-api/endpoints/metrics.api";
|
||||
|
||||
export enum MetricType {
|
||||
MEMORY = "memory",
|
||||
@@ -30,11 +31,15 @@ export interface ClusterOverviewStorageState {
|
||||
interface ClusterOverviewStoreDependencies {
|
||||
readonly storage: StorageLayer<ClusterOverviewStorageState>;
|
||||
readonly nodeStore: NodeStore;
|
||||
requestClusterMetricsByNodeNames: RequestClusterMetricsByNodeNames;
|
||||
}
|
||||
|
||||
export class ClusterOverviewStore extends KubeObjectStore<Cluster, ClusterApi> implements ClusterOverviewStorageState {
|
||||
@observable metrics: Partial<ClusterMetricData> = {};
|
||||
@observable metricsLoaded = false;
|
||||
@observable metrics: ClusterMetricData | undefined = undefined;
|
||||
|
||||
get metricsLoaded() {
|
||||
return !!this.metrics;
|
||||
}
|
||||
|
||||
get metricType(): MetricType {
|
||||
return this.dependencies.storage.get().metricType;
|
||||
@@ -64,9 +69,10 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster, ClusterApi> i
|
||||
// TODO: refactor, seems not a correct place to be
|
||||
// auto-refresh metrics on user-action
|
||||
reaction(() => this.metricNodeRole, () => {
|
||||
if (!this.metricsLoaded) return;
|
||||
this.resetMetrics();
|
||||
this.loadMetrics();
|
||||
if (this.metrics) {
|
||||
this.resetMetrics();
|
||||
this.loadMetrics();
|
||||
}
|
||||
});
|
||||
|
||||
// check which node type to select
|
||||
@@ -79,13 +85,12 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster, ClusterApi> i
|
||||
}
|
||||
|
||||
@action
|
||||
async loadMetrics(params?: IMetricsReqParams) {
|
||||
async loadMetrics(params?: RequestMetricsParams) {
|
||||
await when(() => this.dependencies.nodeStore.isLoaded);
|
||||
const { masterNodes, workerNodes } = this.dependencies.nodeStore;
|
||||
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
|
||||
|
||||
this.metrics = await getMetricsByNodeNames(nodes.map(node => node.getName()), params);
|
||||
this.metricsLoaded = true;
|
||||
this.metrics = await this.dependencies.requestClusterMetricsByNodeNames(nodes.map(node => node.getName()), params);
|
||||
}
|
||||
|
||||
getMetricsValues(source: Partial<ClusterMetricData>): [number, string][] {
|
||||
@@ -101,8 +106,7 @@ export class ClusterOverviewStore extends KubeObjectStore<Cluster, ClusterApi> i
|
||||
|
||||
@action
|
||||
resetMetrics() {
|
||||
this.metrics = {};
|
||||
this.metricsLoaded = false;
|
||||
this.metrics = undefined;
|
||||
}
|
||||
|
||||
reset() {
|
||||
|
||||
@@ -23,6 +23,7 @@ import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-ove
|
||||
import nodeStoreInjectable from "../+nodes/store.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import activeThemeInjectable from "../../themes/active.injectable";
|
||||
import type { ClusterMetricData } from "../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable";
|
||||
|
||||
function createLabels(rawLabelData: [string, number | undefined][]): string[] {
|
||||
return rawLabelData.map(([key, value]) => `${key}: ${value?.toFixed(2) || "N/A"}`);
|
||||
@@ -48,16 +49,32 @@ const NonInjectedClusterPieCharts = observer(({
|
||||
);
|
||||
};
|
||||
|
||||
const renderCharts = () => {
|
||||
const data = getMetricLastPoints(clusterOverviewStore.metrics);
|
||||
const { memoryUsage, memoryRequests, memoryAllocatableCapacity, memoryCapacity, memoryLimits } = data;
|
||||
const { cpuUsage, cpuRequests, cpuAllocatableCapacity, cpuCapacity, cpuLimits } = data;
|
||||
const { podUsage, podAllocatableCapacity, podCapacity } = data;
|
||||
const renderCharts = (lastPoints: Partial<Record<keyof ClusterMetricData, number>>) => {
|
||||
const {
|
||||
memoryUsage, memoryRequests, memoryAllocatableCapacity, memoryCapacity, memoryLimits,
|
||||
cpuUsage, cpuRequests, cpuAllocatableCapacity, cpuCapacity, cpuLimits,
|
||||
podUsage, podAllocatableCapacity, podCapacity,
|
||||
} = lastPoints;
|
||||
|
||||
if (
|
||||
typeof cpuCapacity !== "number" ||
|
||||
typeof cpuAllocatableCapacity !== "number" ||
|
||||
typeof cpuLimits !== "number" ||
|
||||
typeof podCapacity !== "number" ||
|
||||
typeof podAllocatableCapacity !== "number" ||
|
||||
typeof memoryAllocatableCapacity !== "number" ||
|
||||
typeof memoryCapacity !== "number" ||
|
||||
typeof memoryLimits !== "number" ||
|
||||
typeof memoryUsage !== "number" ||
|
||||
typeof memoryRequests !== "number"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cpuLimitsOverload = cpuLimits > cpuAllocatableCapacity;
|
||||
const memoryLimitsOverload = memoryLimits > memoryAllocatableCapacity;
|
||||
const defaultColor = activeTheme.get().colors.pieChartDefaultColor;
|
||||
|
||||
if (!memoryCapacity || !cpuCapacity || !podCapacity || !memoryAllocatableCapacity || !cpuAllocatableCapacity || !podAllocatableCapacity) return null;
|
||||
const cpuData: PieChartData = {
|
||||
datasets: [
|
||||
{
|
||||
@@ -218,7 +235,7 @@ const NonInjectedClusterPieCharts = observer(({
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = ({ metricNodeRole, metricsLoaded }: ClusterOverviewStore) => {
|
||||
const renderContent = ({ metricNodeRole, metrics }: ClusterOverviewStore) => {
|
||||
const { masterNodes, workerNodes } = nodeStore;
|
||||
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
|
||||
|
||||
@@ -231,14 +248,16 @@ const NonInjectedClusterPieCharts = observer(({
|
||||
);
|
||||
}
|
||||
|
||||
if (!metricsLoaded) {
|
||||
if (!metrics) {
|
||||
return (
|
||||
<div className={cssNames(styles.empty, "flex justify-center align-center box grow")}>
|
||||
<Spinner/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterOverviewStore.metrics);
|
||||
|
||||
const lastPoints = getMetricLastPoints(metrics);
|
||||
const { memoryCapacity, cpuCapacity, podCapacity } = lastPoints;
|
||||
|
||||
if (!memoryCapacity || !cpuCapacity || !podCapacity) {
|
||||
return (
|
||||
@@ -248,7 +267,7 @@ const NonInjectedClusterPieCharts = observer(({
|
||||
);
|
||||
}
|
||||
|
||||
return renderCharts();
|
||||
return renderCharts(lastPoints);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export { apiBase, apiKube } from "../../common/k8s-api";
|
||||
export * from "./leases";
|
||||
export * from "./lease-details";
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export * from "./preferences";
|
||||
.LeaseDetails {}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./lease-details.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { DrawerItem } from "../drawer";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import type { Lease } from "../../../common/k8s-api/endpoints";
|
||||
|
||||
export interface LeaseDetailsProps extends KubeObjectDetailsProps<Lease> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class LeaseDetails extends React.Component<LeaseDetailsProps> {
|
||||
|
||||
render() {
|
||||
const { object: lease } = this.props;
|
||||
|
||||
return (
|
||||
<div className="LeaseDetails">
|
||||
<KubeObjectMeta object={lease} />
|
||||
|
||||
<DrawerItem name="Holder Identity">
|
||||
{lease.getHolderIdentity()}
|
||||
</DrawerItem>
|
||||
|
||||
<DrawerItem name="Lease Duration Seconds">
|
||||
{lease.getLeaseDurationSeconds()}
|
||||
</DrawerItem>
|
||||
|
||||
<DrawerItem name="Lease Transitions" hidden={lease.getLeaseTransitions() === undefined}>
|
||||
{lease.getLeaseTransitions()}
|
||||
</DrawerItem>
|
||||
|
||||
<DrawerItem name="Acquire Time" hidden={lease.getAcquireTime() === ""}>
|
||||
{lease.getAcquireTime()}
|
||||
</DrawerItem>
|
||||
|
||||
<DrawerItem name="Renew Time">
|
||||
{lease.getRenewTime()}
|
||||
</DrawerItem>
|
||||
|
||||
</div >
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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 { Leases } from "./leases";
|
||||
import leasesRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/leases/leases-route.injectable";
|
||||
import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
|
||||
|
||||
const leasesRouteComponentInjectable = getInjectable({
|
||||
id: "leases-route-component",
|
||||
|
||||
instantiate: (di) => ({
|
||||
route: di.inject(leasesRouteInjectable),
|
||||
Component: Leases,
|
||||
}),
|
||||
|
||||
injectionToken: routeSpecificComponentInjectionToken,
|
||||
});
|
||||
|
||||
export default leasesRouteComponentInjectable;
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 { computed } from "mobx";
|
||||
|
||||
import leasesRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/leases/leases-route.injectable";
|
||||
import { configSidebarItemId } from "../+config/config-sidebar-items.injectable";
|
||||
import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable";
|
||||
import routeIsActiveInjectable from "../../routes/route-is-active.injectable";
|
||||
import navigateToLeasesInjectable from "../../../common/front-end-routing/routes/cluster/config/leases/navigate-to-leases.injectable";
|
||||
|
||||
const leasesSidebarItemsInjectable = getInjectable({
|
||||
id: "leases-sidebar-items",
|
||||
|
||||
instantiate: (di) => {
|
||||
const route = di.inject(leasesRouteInjectable);
|
||||
const navigateToLeases = di.inject(navigateToLeasesInjectable);
|
||||
const routeIsActive = di.inject(routeIsActiveInjectable, route);
|
||||
|
||||
return computed(() => [
|
||||
{
|
||||
id: "leases",
|
||||
parentId: configSidebarItemId,
|
||||
title: "Leases",
|
||||
onClick: navigateToLeases,
|
||||
isActive: routeIsActive,
|
||||
isVisible: route.isEnabled,
|
||||
orderNumber: 80,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
injectionToken: sidebarItemsInjectionToken,
|
||||
});
|
||||
|
||||
export default leasesSidebarItemsInjectable;
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
.Leases {
|
||||
.TableCell {
|
||||
&.name {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@include table-cell-warning;
|
||||
}
|
||||
|
||||
&.keys {
|
||||
flex: 2.5;
|
||||
}
|
||||
|
||||
&.age {
|
||||
flex: .5;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./leases.scss";
|
||||
|
||||
import * as React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { Lease } from "../../../common/k8s-api/endpoints/lease.api";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout";
|
||||
import { KubeObjectAge } from "../kube-object/age";
|
||||
import { autoBind } from "../../../common/utils";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import leaseStoreInjectable from "./store.injectable";
|
||||
import type { LeaseStore } from "./store";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
holder = "holder",
|
||||
age = "age",
|
||||
}
|
||||
|
||||
export interface LeaseProps extends KubeObjectDetailsProps<Lease> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
leaseStore: LeaseStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedLease extends React.Component<LeaseProps & Dependencies> {
|
||||
constructor(props: LeaseProps & Dependencies) {
|
||||
super(props);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { leaseStore } = this.props;
|
||||
|
||||
return (
|
||||
<SiblingsInTabLayout>
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="configuration_leases"
|
||||
className="Leases"
|
||||
store={leaseStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: lease => lease.getName(),
|
||||
[columnId.namespace]: lease => lease.getNs(),
|
||||
[columnId.holder]: lease => lease.getHolderIdentity(),
|
||||
[columnId.age]: lease => -lease.getCreationTimestamp(),
|
||||
}}
|
||||
searchFilters={[
|
||||
lease => lease.getSearchFields(),
|
||||
]}
|
||||
renderHeaderTitle="Leases"
|
||||
renderTableHeader={[
|
||||
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||
{ className: "warning", showWithColumn: columnId.name },
|
||||
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||
{ title: "Holder", className: "holder", sortBy: columnId.holder, id: columnId.holder },
|
||||
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||
]}
|
||||
renderTableContents={lease => [
|
||||
lease.getName(),
|
||||
<KubeObjectStatusIcon key="icon" object={lease} />,
|
||||
lease.getNs(),
|
||||
lease.getHolderIdentity(),
|
||||
<KubeObjectAge key="age" object={lease} />,
|
||||
]}
|
||||
/>
|
||||
</SiblingsInTabLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const Leases = withInjectables<Dependencies, LeaseProps>(NonInjectedLease, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
leaseStore: di.inject(leaseStoreInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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 assert from "assert";
|
||||
import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import leaseApiInjectable from "../../../common/k8s-api/endpoints/lease.api.injectable";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
||||
import { LeaseStore } from "./store";
|
||||
|
||||
const leaseStoreInjectable = getInjectable({
|
||||
id: "lease-store",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectable), "leaseStore is only available in certain environments");
|
||||
|
||||
const api = di.inject(leaseApiInjectable);
|
||||
|
||||
return new LeaseStore(api);
|
||||
},
|
||||
injectionToken: kubeObjectStoreInjectionToken,
|
||||
});
|
||||
|
||||
export default leaseStoreInjectable;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { Lease, LeaseApi } from "../../../common/k8s-api/endpoints/lease.api";
|
||||
|
||||
export class LeaseStore extends KubeObjectStore<Lease, LeaseApi> {
|
||||
}
|
||||
+1
-1
@@ -27,7 +27,7 @@ const priorityClassesSidebarItemsInjectable = getInjectable({
|
||||
onClick: navigateToPriorityClasses,
|
||||
isActive: routeIsActive,
|
||||
isVisible: route.isEnabled,
|
||||
orderNumber: 60,
|
||||
orderNumber: 70,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import { PriorityClasses } from "./priority-classes";
|
||||
import priorityClassesRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/priority-classes/priority-classes-route.injectable";
|
||||
import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
|
||||
|
||||
const podDisruptionBudgetsRouteComponentInjectable = getInjectable({
|
||||
const priorityClassesRouteComponentInjectable = getInjectable({
|
||||
id: "priority-classes-route-component",
|
||||
|
||||
instantiate: (di) => ({
|
||||
@@ -18,4 +18,4 @@ const podDisruptionBudgetsRouteComponentInjectable = getInjectable({
|
||||
injectionToken: routeSpecificComponentInjectionToken,
|
||||
});
|
||||
|
||||
export default podDisruptionBudgetsRouteComponentInjectable;
|
||||
export default priorityClassesRouteComponentInjectable;
|
||||
+2
-5
@@ -3,8 +3,5 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
.Preferences {
|
||||
.loading-spinner {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
export * from "./runtime-classes";
|
||||
export * from "./runtime-classes-details";
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
.RuntimeClassDetailsTolerations {
|
||||
grid-template-columns: auto;
|
||||
|
||||
.RuntimeClassTolerations {
|
||||
margin-top: var(--margin);
|
||||
}
|
||||
|
||||
// Expanding value cell to cover 2 columns (whole Drawer width)
|
||||
|
||||
>.name {
|
||||
grid-row-start: 1;
|
||||
grid-column-start: 1;
|
||||
}
|
||||
|
||||
>.value {
|
||||
grid-row-start: 1;
|
||||
grid-column-start: 1;
|
||||
}
|
||||
|
||||
.DrawerParamToggler>.params {
|
||||
margin-left: var(--drawer-item-title-width);
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./runtime-classes-details-tolerations.scss";
|
||||
import React from "react";
|
||||
import { DrawerParamToggler, DrawerItem } from "../drawer";
|
||||
import type { Toleration, KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { RuntimeClassTolerations } from "./runtime-classes-tolerations";
|
||||
|
||||
export interface KubeObjectWithTolerations extends KubeObject {
|
||||
getTolerations(): Toleration[];
|
||||
}
|
||||
|
||||
export interface RuntimeClassDetailsTolerationsProps {
|
||||
runtimeClass: KubeObjectWithTolerations;
|
||||
}
|
||||
|
||||
export function RuntimeClassDetailsTolerations({ runtimeClass: runtimeClass }: RuntimeClassDetailsTolerationsProps) {
|
||||
const tolerations = runtimeClass.getTolerations();
|
||||
|
||||
if (!tolerations.length) return null;
|
||||
|
||||
return (
|
||||
<DrawerItem name="Tolerations" className="RuntimeClassDetailsTolerations">
|
||||
<DrawerParamToggler label={tolerations.length}>
|
||||
<RuntimeClassTolerations tolerations={tolerations} />
|
||||
</DrawerParamToggler>
|
||||
</DrawerItem>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export * from "./update-button";
|
||||
.RuntimeClassesDetails {}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./runtime-classes.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { DrawerItem } from "../drawer";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { RuntimeClass } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { Badge } from "../badge";
|
||||
import { RuntimeClassDetailsTolerations } from "./runtime-classes-details-tolerations";
|
||||
|
||||
export interface RuntimeClassesDetailsProps extends KubeObjectDetailsProps<RuntimeClass> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class RuntimeClassesDetails extends React.Component<RuntimeClassesDetailsProps> {
|
||||
|
||||
render() {
|
||||
const { object: rc } = this.props;
|
||||
const nodeSelector = rc.getNodeSelectors();
|
||||
|
||||
return (
|
||||
<div className="RuntimeClassesDetails">
|
||||
<KubeObjectMeta object={rc} />
|
||||
|
||||
<DrawerItem name="Handler">
|
||||
{rc.getHandler()}
|
||||
</DrawerItem>
|
||||
|
||||
<DrawerItem name="Pod Fixed" hidden={rc.getPodFixed() === ""}>
|
||||
{rc.getPodFixed()}
|
||||
</DrawerItem>
|
||||
|
||||
<DrawerItem name="Node Selector" hidden={nodeSelector.length === 0}>
|
||||
{nodeSelector.map(label => <Badge key={label} label={label} />)}
|
||||
</DrawerItem>
|
||||
|
||||
<RuntimeClassDetailsTolerations runtimeClass={rc} />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 { computed } from "mobx";
|
||||
|
||||
import runtimeClassesRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/runtime-classes/runtime-classes-route.injectable";
|
||||
import { configSidebarItemId } from "../+config/config-sidebar-items.injectable";
|
||||
import { sidebarItemsInjectionToken } from "../layout/sidebar-items.injectable";
|
||||
import routeIsActiveInjectable from "../../routes/route-is-active.injectable";
|
||||
import navigateToRuntimeClassesInjectable from "../../../common/front-end-routing/routes/cluster/config/runtime-classes/navigate-to-runtime-classes.injectable";
|
||||
|
||||
const runtimeClassesSidebarItemsInjectable = getInjectable({
|
||||
id: "runtime-classes-sidebar-items",
|
||||
|
||||
instantiate: (di) => {
|
||||
const route = di.inject(runtimeClassesRouteInjectable);
|
||||
const navigateToRuntimeClasses = di.inject(navigateToRuntimeClassesInjectable);
|
||||
const routeIsActive = di.inject(routeIsActiveInjectable, route);
|
||||
|
||||
return computed(() => [
|
||||
{
|
||||
id: "runtime-classes",
|
||||
parentId: configSidebarItemId,
|
||||
title: "Runtime Classes",
|
||||
onClick: navigateToRuntimeClasses,
|
||||
isActive: routeIsActive,
|
||||
isVisible: route.isEnabled,
|
||||
orderNumber: 70,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
injectionToken: sidebarItemsInjectionToken,
|
||||
});
|
||||
|
||||
export default runtimeClassesSidebarItemsInjectable;
|
||||
+7
-7
@@ -3,19 +3,19 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { RuntimeClasses } from "./runtime-classes";
|
||||
import runtimeClassesRouteInjectable from "../../../common/front-end-routing/routes/cluster/config/runtime-classes/runtime-classes-route.injectable";
|
||||
import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
|
||||
import terminalPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/terminal/terminal-preferences-route.injectable";
|
||||
import { Terminal } from "./terminal";
|
||||
|
||||
const terminalPreferencesRouteComponentInjectable = getInjectable({
|
||||
id: "terminal-preferences-route-component",
|
||||
const runtimeClassesRouteComponentInjectable = getInjectable({
|
||||
id: "runtime-classes-route-component",
|
||||
|
||||
instantiate: (di) => ({
|
||||
route: di.inject(terminalPreferencesRouteInjectable),
|
||||
Component: Terminal,
|
||||
route: di.inject(runtimeClassesRouteInjectable),
|
||||
Component: RuntimeClasses,
|
||||
}),
|
||||
|
||||
injectionToken: routeSpecificComponentInjectionToken,
|
||||
});
|
||||
|
||||
export default terminalPreferencesRouteComponentInjectable;
|
||||
export default runtimeClassesRouteComponentInjectable;
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
.RuntimeClassTolerations {
|
||||
.TableHead {
|
||||
background-color: var(--drawerSubtitleBackground);
|
||||
}
|
||||
|
||||
.TableCell {
|
||||
white-space: normal;
|
||||
word-break: normal;
|
||||
|
||||
&.key {
|
||||
flex-grow: 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./runtime-classes-tolerations.scss";
|
||||
import React from "react";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
|
||||
import type { Toleration } from "../../../common/k8s-api/kube-object";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
|
||||
export interface RuntimeClassTolerationsProps {
|
||||
tolerations: Toleration[];
|
||||
}
|
||||
|
||||
enum sortBy {
|
||||
Key = "key",
|
||||
Operator = "operator",
|
||||
Effect = "effect",
|
||||
Seconds = "seconds",
|
||||
Value = "value",
|
||||
}
|
||||
|
||||
const getTableRow = (toleration: Toleration) => {
|
||||
const { key, operator, effect, tolerationSeconds, value } = toleration;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={uniqueId("toleration-")}
|
||||
sortItem={toleration}
|
||||
nowrap
|
||||
>
|
||||
<TableCell className="key">{key}</TableCell>
|
||||
<TableCell className="operator">{operator}</TableCell>
|
||||
<TableCell className="value">{value}</TableCell>
|
||||
<TableCell className="effect">{effect}</TableCell>
|
||||
<TableCell className="seconds">{tolerationSeconds}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export function RuntimeClassTolerations({ tolerations }: RuntimeClassTolerationsProps) {
|
||||
return (
|
||||
<Table
|
||||
tableId="workloads_pod_tolerations"
|
||||
selectable
|
||||
items={tolerations}
|
||||
scrollable={false}
|
||||
sortable={{
|
||||
[sortBy.Key]: toleration => toleration.key,
|
||||
[sortBy.Operator]: toleration => toleration.operator,
|
||||
[sortBy.Effect]: toleration => toleration.effect,
|
||||
[sortBy.Seconds]: toleration => toleration.tolerationSeconds,
|
||||
}}
|
||||
sortSyncWithUrl={false}
|
||||
className="RuntimeClassTolerations"
|
||||
renderRow={getTableRow}
|
||||
>
|
||||
<TableHead sticky={false}>
|
||||
<TableCell className="key" sortBy={sortBy.Key}>Key</TableCell>
|
||||
<TableCell className="operator" sortBy={sortBy.Operator}>Operator</TableCell>
|
||||
<TableCell className="value" sortBy={sortBy.Value}>Value</TableCell>
|
||||
<TableCell className="effect" sortBy={sortBy.Effect}>Effect</TableCell>
|
||||
<TableCell className="seconds" sortBy={sortBy.Seconds}>Seconds</TableCell>
|
||||
</TableHead>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
.RuntimeClasses {
|
||||
.TableCell {
|
||||
&.name {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@include table-cell-warning;
|
||||
}
|
||||
|
||||
&.keys {
|
||||
flex: 2.5;
|
||||
}
|
||||
|
||||
&.age {
|
||||
flex: .5;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import "./runtime-classes.scss";
|
||||
|
||||
import * as React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { RuntimeClass } from "../../../common/k8s-api/endpoints/runtime-class.api";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout";
|
||||
import { KubeObjectAge } from "../kube-object/age";
|
||||
import { autoBind } from "../../../common/utils";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import runtimeClassStoreInjectable from "./store.injectable";
|
||||
import type { RuntimeClassStore } from "./store";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
handler = "handler",
|
||||
age = "age",
|
||||
}
|
||||
|
||||
export interface RuntimeClassesProps extends KubeObjectDetailsProps<RuntimeClass> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
runtimeClassStore: RuntimeClassStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedRuntimeClasses extends React.Component<RuntimeClassesProps & Dependencies> {
|
||||
constructor(props: RuntimeClassesProps & Dependencies) {
|
||||
super(props);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { runtimeClassStore } = this.props;
|
||||
|
||||
return (
|
||||
<SiblingsInTabLayout>
|
||||
<KubeObjectListLayout
|
||||
isConfigurable
|
||||
tableId="configuration_runtime_classes"
|
||||
className="RuntimeClasses"
|
||||
store={runtimeClassStore}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: rc => rc.getName(),
|
||||
[columnId.handler]: rc => rc.getHandler(),
|
||||
[columnId.age]: rc => -rc.getCreationTimestamp(),
|
||||
}}
|
||||
searchFilters={[
|
||||
rc => rc.getSearchFields(),
|
||||
]}
|
||||
renderHeaderTitle="Runtime Classes"
|
||||
renderTableHeader={[
|
||||
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||
{ className: "warning", showWithColumn: columnId.name },
|
||||
{ title: "Handler", className: "handler", sortBy: columnId.handler, id: columnId.handler },
|
||||
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
|
||||
]}
|
||||
renderTableContents={rc => [
|
||||
rc.getName(),
|
||||
<KubeObjectStatusIcon key="icon" object={rc} />,
|
||||
rc.getHandler(),
|
||||
<KubeObjectAge key="age" object={rc} />,
|
||||
]}
|
||||
/>
|
||||
</SiblingsInTabLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const RuntimeClasses = withInjectables<Dependencies, RuntimeClassesProps>(NonInjectedRuntimeClasses, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
runtimeClassStore: di.inject(runtimeClassStoreInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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 assert from "assert";
|
||||
import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import runtimeClassApiInjectable from "../../../common/k8s-api/endpoints/runtime-class.api.injectable";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
||||
import { RuntimeClassStore } from "./store";
|
||||
|
||||
const runtimeClassStoreInjectable = getInjectable({
|
||||
id: "runtime-class-store",
|
||||
instantiate: (di) => {
|
||||
assert(di.inject(storesAndApisCanBeCreatedInjectable), "runtimeClassStore is only available in certain environments");
|
||||
|
||||
const api = di.inject(runtimeClassApiInjectable);
|
||||
|
||||
return new RuntimeClassStore(api);
|
||||
},
|
||||
injectionToken: kubeObjectStoreInjectionToken,
|
||||
});
|
||||
|
||||
export default runtimeClassStoreInjectable;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import type { RuntimeClass, RuntimeClassApi } from "../../../common/k8s-api/endpoints/runtime-class.api";
|
||||
|
||||
export class RuntimeClassStore extends KubeObjectStore<RuntimeClass, RuntimeClassApi> {
|
||||
}
|
||||
+4
-20
@@ -45,11 +45,7 @@ exports[`<CustomResourceDetails /> with a CRD with a boolean field should displa
|
||||
<span
|
||||
class="value"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
false
|
||||
</li>
|
||||
</ul>
|
||||
false
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,11 +97,7 @@ exports[`<CustomResourceDetails /> with a CRD with a boolean field should displa
|
||||
<span
|
||||
class="value"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
true
|
||||
</li>
|
||||
</ul>
|
||||
true
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,11 +149,7 @@ exports[`<CustomResourceDetails /> with a CRD with a number field should display
|
||||
<span
|
||||
class="value"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
0
|
||||
</li>
|
||||
</ul>
|
||||
0
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,11 +201,7 @@ exports[`<CustomResourceDetails /> with a CRD with a number field should display
|
||||
<span
|
||||
class="value"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
1234
|
||||
</li>
|
||||
</ul>
|
||||
1234
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,11 +15,10 @@ import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { Input } from "../input";
|
||||
import type { AdditionalPrinterColumnsV1 } from "../../../common/k8s-api/endpoints/custom-resource-definition.api";
|
||||
import { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/custom-resource-definition.api";
|
||||
import { convertKubectlJsonPathToNodeJsonPath } from "../../utils/jsonPath";
|
||||
import { safeJSONPathValue } from "../../utils/jsonPath";
|
||||
import type { KubeObjectMetadata, KubeObjectStatus } from "../../../common/k8s-api/kube-object";
|
||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import logger from "../../../common/logger";
|
||||
import { JSONPath } from "@astronautlabs/jsonpath";
|
||||
|
||||
export interface CustomResourceDetailsProps extends KubeObjectDetailsProps<KubeObject> {
|
||||
crd: CustomResourceDefinition;
|
||||
@@ -66,7 +65,7 @@ export class CustomResourceDetails extends React.Component<CustomResourceDetails
|
||||
renderAdditionalColumns(resource: KubeObject, columns: AdditionalPrinterColumnsV1[]) {
|
||||
return columns.map(({ name, jsonPath }) => (
|
||||
<DrawerItem key={name} name={name}>
|
||||
{convertSpecValue(JSONPath.query(resource, convertKubectlJsonPathToNodeJsonPath(jsonPath)))}
|
||||
{convertSpecValue(safeJSONPathValue(resource, jsonPath))}
|
||||
</DrawerItem>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { computed, makeObservable } from "mobx";
|
||||
import type { ApiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { safeJSONPathValue } from "../../utils/jsonPath";
|
||||
import { formatJSONValue, safeJSONPathValue } from "../../utils/jsonPath";
|
||||
import { TabLayout } from "../layout/tab-layout-2";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import customResourcesRouteParametersInjectable from "./custom-resources-route-parameters.injectable";
|
||||
@@ -73,7 +73,7 @@ class NonInjectedCustomResources extends React.Component<Dependencies> {
|
||||
[columnId.age]: customResource => -customResource.getCreationTimestamp(),
|
||||
...Object.fromEntries(extraColumns.map(({ name, jsonPath }) => [
|
||||
name,
|
||||
customResource => safeJSONPathValue(customResource, jsonPath),
|
||||
customResource => formatJSONValue(safeJSONPathValue(customResource, jsonPath)),
|
||||
])),
|
||||
}}
|
||||
searchFilters={[
|
||||
|
||||
@@ -16,3 +16,9 @@
|
||||
.settingsAvatar {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.avatarAndName {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--margin);
|
||||
}
|
||||
@@ -86,7 +86,7 @@ class NonInjectedEntitySettings extends React.Component<Dependencies> {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center pb-8">
|
||||
<div className={styles.avatarAndName}>
|
||||
<Avatar
|
||||
title={entity.getName()}
|
||||
colorHash={`${entity.getName()}-${entity.metadata.source}`}
|
||||
|
||||
@@ -5,14 +5,11 @@
|
||||
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import fse from "fs-extra";
|
||||
import React from "react";
|
||||
import type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery";
|
||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
import { ConfirmDialog } from "../../confirm-dialog";
|
||||
import { Extensions } from "../extensions";
|
||||
import mockFs from "mock-fs";
|
||||
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import type { DiRender } from "../../test-utils/renderFor";
|
||||
@@ -20,41 +17,28 @@ import { renderFor } from "../../test-utils/renderFor";
|
||||
import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import directoryForDownloadsInjectable from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable";
|
||||
import getConfigurationFileModelInjectable from "../../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import assert from "assert";
|
||||
import type { InstallFromInput } from "../install-from-input/install-from-input";
|
||||
import installFromInputInjectable from "../install-from-input/install-from-input.injectable";
|
||||
import type { InstallExtensionFromInput } from "../install-extension-from-input.injectable";
|
||||
import installExtensionFromInputInjectable from "../install-extension-from-input.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import { observable, when } from "mobx";
|
||||
|
||||
mockWindow();
|
||||
|
||||
jest.setTimeout(30000);
|
||||
jest.mock("fs-extra");
|
||||
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 { DeleteFile } from "../../../../common/fs/delete-file.injectable";
|
||||
import deleteFileInjectable from "../../../../common/fs/delete-file.injectable";
|
||||
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;
|
||||
let extensionDiscovery: ExtensionDiscovery;
|
||||
let installFromInput: jest.MockedFunction<InstallFromInput>;
|
||||
let installExtensionFromInput: jest.MockedFunction<InstallExtensionFromInput>;
|
||||
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 });
|
||||
@@ -62,17 +46,19 @@ describe("Extensions", () => {
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
di.override(directoryForDownloadsInjectable, () => "some-directory-for-downloads");
|
||||
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
|
||||
mockFs({
|
||||
"some-directory-for-user-data": {},
|
||||
});
|
||||
|
||||
render = renderFor(di);
|
||||
|
||||
installFromInput = jest.fn();
|
||||
installExtensionFromInput = jest.fn();
|
||||
di.override(installExtensionFromInputInjectable, () => installExtensionFromInput);
|
||||
|
||||
di.override(installFromInputInjectable, () => installFromInput);
|
||||
deleteFileMock = jest.fn();
|
||||
di.override(deleteFileInjectable, () => deleteFileMock);
|
||||
|
||||
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);
|
||||
@@ -95,10 +81,6 @@ describe("Extensions", () => {
|
||||
extensionDiscovery.uninstallExtension = jest.fn(() => Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("disables uninstall and disable buttons while uninstalling", async () => {
|
||||
extensionDiscovery.isLoaded = true;
|
||||
|
||||
@@ -123,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");
|
||||
@@ -137,9 +119,10 @@ describe("Extensions", () => {
|
||||
render(<Extensions />);
|
||||
|
||||
const resolveInstall = observable.box(false);
|
||||
const url = "https://test.extensionurl/package.tgz";
|
||||
|
||||
(fse.unlink as jest.MockedFunction<typeof fse.unlink>).mockReturnValue(Promise.resolve());
|
||||
installFromInput.mockImplementation(async (input) => {
|
||||
deleteFileMock.mockReturnValue(Promise.resolve());
|
||||
installExtensionFromInput.mockImplementation(async (input) => {
|
||||
expect(input).toBe("https://test.extensionurl/package.tgz");
|
||||
|
||||
const clear = extensionInstallationStateStore.startPreInstall();
|
||||
@@ -152,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,22 +2,23 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { ExtendableDisposer } from "../../../common/utils";
|
||||
import { downloadFile, downloadJson } from "../../../common/utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import { isObject } from "../../../common/utils";
|
||||
import React from "react";
|
||||
import path from "path";
|
||||
import { SemVer } from "semver";
|
||||
import URLParse from "url-parse";
|
||||
import type { InstallRequest } from "./attempt-install/install-request";
|
||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import type { Confirm } from "../confirm-dialog/confirm.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import attemptInstallInjectable from "./attempt-install/attempt-install.injectable";
|
||||
import getBaseRegistryUrlInjectable from "./get-base-registry-url/get-base-registry-url.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import confirmInjectable from "../confirm-dialog/confirm.injectable";
|
||||
import { reduce } from "lodash";
|
||||
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
||||
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;
|
||||
@@ -25,128 +26,180 @@ export interface ExtensionInfo {
|
||||
requireConfirmation?: boolean;
|
||||
}
|
||||
|
||||
export type AttemptInstallByInfo = (info: ExtensionInfo) => Promise<void>;
|
||||
|
||||
interface Dependencies {
|
||||
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>;
|
||||
getBaseRegistryUrl: () => Promise<string>;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
confirm: Confirm;
|
||||
interface NpmPackageVersionDescriptor extends PackageJson {
|
||||
dist: {
|
||||
integrity: string;
|
||||
shasum: string;
|
||||
tarball: string;
|
||||
};
|
||||
}
|
||||
|
||||
const attemptInstallByInfo = ({
|
||||
attemptInstall,
|
||||
getBaseRegistryUrl,
|
||||
extensionInstallationStateStore,
|
||||
confirm,
|
||||
}: Dependencies): AttemptInstallByInfo => (
|
||||
async (info) => {
|
||||
const { name, version, requireConfirmation = false } = info;
|
||||
const disposer = extensionInstallationStateStore.startPreInstall();
|
||||
const baseUrl = await getBaseRegistryUrl();
|
||||
const registryUrl = new URLParse(baseUrl).set("pathname", name).toString();
|
||||
let json: any;
|
||||
let finalVersion = version;
|
||||
interface NpmRegistryPackageDescriptor {
|
||||
versions: Partial<Record<string, NpmPackageVersionDescriptor>>;
|
||||
"dist-tags"?: Partial<Record<string, string>>;
|
||||
}
|
||||
|
||||
try {
|
||||
json = await downloadJson({ url: registryUrl }).promise;
|
||||
export type AttemptInstallByInfo = (info: ExtensionInfo) => Promise<void>;
|
||||
|
||||
if (!json || json.error || typeof json.versions !== "object" || !json.versions) {
|
||||
const message = json?.error ? `: ${json.error}` : "";
|
||||
const attemptInstallByInfoInjectable = getInjectable({
|
||||
id: "attempt-install-by-info",
|
||||
instantiate: (di): AttemptInstallByInfo => {
|
||||
const attemptInstall = di.inject(attemptInstallInjectable);
|
||||
const getBaseRegistryUrl = di.inject(getBaseRegistryUrlInjectable);
|
||||
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||
const confirm = di.inject(confirmInjectable);
|
||||
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
|
||||
const downloadJson = di.inject(downloadJsonInjectable);
|
||||
const downloadBinary = di.inject(downloadBinaryInjectable);
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
Notifications.error(`Failed to get registry information for that extension${message}`);
|
||||
return async (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: NpmRegistryPackageDescriptor;
|
||||
|
||||
try {
|
||||
const result = await downloadJson(registryUrl);
|
||||
|
||||
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
|
||||
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 {
|
||||
logger.error("Failed to download registry information", error);
|
||||
showErrorNotification(`Failed to get valid registry information for extension. ${error}`);
|
||||
}
|
||||
|
||||
return disposer();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
// assume invalid JSON
|
||||
console.warn("Set registry has invalid json", { url: baseUrl }, error);
|
||||
Notifications.error("Failed to get valid registry information for that extension. Registry did not return valid JSON");
|
||||
} else {
|
||||
console.error("Failed to download registry information", error);
|
||||
Notifications.error(`Failed to get valid registry information for that extension. ${error}`);
|
||||
}
|
||||
|
||||
return disposer();
|
||||
}
|
||||
let version = versionOrTagName;
|
||||
|
||||
if (version) {
|
||||
if (!json.versions[version]) {
|
||||
if (json["dist-tags"][version]) {
|
||||
finalVersion = json["dist-tags"][version];
|
||||
} else {
|
||||
Notifications.error((
|
||||
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>
|
||||
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>{version}</code>
|
||||
<code>{versionOrTagName}</code>
|
||||
.
|
||||
</p>
|
||||
));
|
||||
|
||||
return disposer();
|
||||
}
|
||||
} else {
|
||||
const versions = Object.keys(json.versions)
|
||||
.map(version => new SemVer(version, { loose: true }))
|
||||
// ignore pre-releases for auto picking the version
|
||||
.filter(version => version.prerelease.length === 0);
|
||||
|
||||
const latestVersion = reduce(versions, (prev, curr) => prev.compareMain(curr) === -1 ? curr : prev);
|
||||
|
||||
version = latestVersion?.format();
|
||||
}
|
||||
} else {
|
||||
const versions = Object.keys(json.versions)
|
||||
.map(version => new SemVer(version, { loose: true }))
|
||||
// ignore pre-releases for auto picking the version
|
||||
.filter(version => version.prerelease.length === 0);
|
||||
|
||||
const latestVersion = reduce(versions, (prev, curr) => prev.compareMain(curr) === -1 ? curr : prev);
|
||||
|
||||
if (!latestVersion) {
|
||||
console.error("No versions supplied for that extension", { name });
|
||||
Notifications.error(`No versions found for ${name}`);
|
||||
if (!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 (requireConfirmation) {
|
||||
const proceed = await confirm({
|
||||
message: (
|
||||
<p>
|
||||
Are you sure you want to install
|
||||
{" "}
|
||||
<b>
|
||||
{name}
|
||||
@
|
||||
{finalVersion}
|
||||
</b>
|
||||
?
|
||||
</p>
|
||||
),
|
||||
labelCancel: "Cancel",
|
||||
labelOk: "Install",
|
||||
});
|
||||
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);
|
||||
|
||||
if (!proceed) {
|
||||
return disposer();
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const url = json.versions[finalVersion!].dist.tarball;
|
||||
const fileName = path.basename(url);
|
||||
const { promise: dataP } = downloadFile({ url, timeout: 10 * 60 * 1000 });
|
||||
if (requireConfirmation) {
|
||||
const proceed = await confirm({
|
||||
message: (
|
||||
<p>
|
||||
{"Are you sure you want to install "}
|
||||
<b>
|
||||
{`${name}@${version}`}
|
||||
</b>
|
||||
?
|
||||
</p>
|
||||
),
|
||||
labelCancel: "Cancel",
|
||||
labelOk: "Install",
|
||||
});
|
||||
|
||||
return attemptInstall({ fileName, dataP }, disposer);
|
||||
}
|
||||
);
|
||||
if (!proceed) {
|
||||
return disposer();
|
||||
}
|
||||
}
|
||||
|
||||
const attemptInstallByInfoInjectable = getInjectable({
|
||||
id: "attempt-install-by-info",
|
||||
instantiate: (di) => attemptInstallByInfo({
|
||||
attemptInstall: di.inject(attemptInstallInjectable),
|
||||
getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
confirm: di.inject(confirmInjectable),
|
||||
}),
|
||||
const fileName = getBasenameOfPath(tarballUrl);
|
||||
const { signal } = withTimeout(10 * 60 * 1000);
|
||||
const request = await downloadBinary(tarballUrl, { signal });
|
||||
|
||||
if (!request.callWasSuccessful) {
|
||||
showErrorNotification(`Failed to download extension: ${request.error}`);
|
||||
|
||||
return disposer();
|
||||
}
|
||||
|
||||
return attemptInstall({ fileName, data: request.response }, disposer);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default attemptInstallByInfoInjectable;
|
||||
@@ -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,138 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type {
|
||||
Disposer,
|
||||
ExtendableDisposer } from "../../../../common/utils";
|
||||
import {
|
||||
disposer,
|
||||
} from "../../../../common/utils";
|
||||
import { 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 { InstallRequestValidated } from "./create-temp-files-and-validate/create-temp-files-and-validate";
|
||||
import type { InstallRequest } from "./install-request";
|
||||
import type {
|
||||
ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import {
|
||||
ExtensionInstallationState,
|
||||
} from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
uninstallExtension: (id: LensExtensionId) => Promise<boolean>;
|
||||
|
||||
unpackExtension: (
|
||||
request: InstallRequestValidated,
|
||||
disposeDownloading: Disposer,
|
||||
) => Promise<void>;
|
||||
|
||||
createTempFilesAndValidate: (
|
||||
installRequest: InstallRequest,
|
||||
) => Promise<InstallRequestValidated | null>;
|
||||
|
||||
getExtensionDestFolder: (name: string) => string;
|
||||
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
export const attemptInstall =
|
||||
({
|
||||
extensionLoader,
|
||||
uninstallExtension,
|
||||
unpackExtension,
|
||||
createTempFilesAndValidate,
|
||||
getExtensionDestFolder,
|
||||
extensionInstallationStateStore,
|
||||
}: Dependencies) =>
|
||||
async (request: InstallRequest, d?: ExtendableDisposer): Promise<void> => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
+97
@@ -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;
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { createTempFilesAndValidate } from "./create-temp-files-and-validate";
|
||||
import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||
|
||||
const createTempFilesAndValidateInjectable = getInjectable({
|
||||
id: "create-temp-files-and-validate",
|
||||
|
||||
instantiate: (di) =>
|
||||
createTempFilesAndValidate({
|
||||
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default createTempFilesAndValidateInjectable;
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { validatePackage } from "../validate-package/validate-package";
|
||||
import type { ExtensionDiscovery } from "../../../../../extensions/extension-discovery/extension-discovery";
|
||||
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
||||
import logger from "../../../../../main/logger";
|
||||
import { Notifications } from "../../../notifications";
|
||||
import path from "path";
|
||||
import fse from "fs-extra";
|
||||
import React from "react";
|
||||
import os from "os";
|
||||
import type {
|
||||
LensExtensionId,
|
||||
LensExtensionManifest,
|
||||
} from "../../../../../extensions/lens-extension";
|
||||
import type { InstallRequest } from "../install-request";
|
||||
|
||||
export interface InstallRequestValidated {
|
||||
fileName: string;
|
||||
data: Buffer;
|
||||
id: LensExtensionId;
|
||||
manifest: LensExtensionManifest;
|
||||
tempFile: string; // temp system path to packed extension for unpacking
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
extensionDiscovery: ExtensionDiscovery;
|
||||
}
|
||||
|
||||
export const createTempFilesAndValidate =
|
||||
({ extensionDiscovery }: Dependencies) =>
|
||||
async ({
|
||||
fileName,
|
||||
dataP,
|
||||
}: InstallRequest): Promise<InstallRequestValidated | null> => {
|
||||
// copy files to temp
|
||||
await fse.ensureDir(getExtensionPackageTemp());
|
||||
|
||||
// validate packages
|
||||
const tempFile = getExtensionPackageTemp(fileName);
|
||||
|
||||
try {
|
||||
const data = await dataP;
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await fse.writeFile(tempFile, data);
|
||||
const manifest = await validatePackage(tempFile);
|
||||
const id = path.join(
|
||||
extensionDiscovery.nodeModulesPath,
|
||||
manifest.name,
|
||||
"package.json",
|
||||
);
|
||||
|
||||
return {
|
||||
fileName,
|
||||
data,
|
||||
manifest,
|
||||
tempFile,
|
||||
id,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(
|
||||
`[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`,
|
||||
{ error },
|
||||
);
|
||||
Notifications.error(
|
||||
<div className="flex column gaps">
|
||||
<p>
|
||||
{"Installing "}
|
||||
<em>{fileName}</em>
|
||||
{" has failed, skipping."}
|
||||
</p>
|
||||
<p>
|
||||
{"Reason: "}
|
||||
<em>{message}</em>
|
||||
</p>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
function getExtensionPackageTemp(fileName = "") {
|
||||
return path.join(os.tmpdir(), "lens-extensions", fileName);
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 { sanitizeExtensionName } from "../../../../extensions/lens-extension";
|
||||
import path from "path";
|
||||
|
||||
export type GetExtensionDestFolder = (name: string) => string;
|
||||
|
||||
const getExtensionDestFolderInjectable = getInjectable({
|
||||
id: "get-extension-dest-folder",
|
||||
|
||||
instantiate: (di): GetExtensionDestFolder => {
|
||||
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
|
||||
return (name) => path.join(extensionDiscovery.localFolderPath, sanitizeExtensionName(name));
|
||||
},
|
||||
});
|
||||
|
||||
export default getExtensionDestFolderInjectable;
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
|
||||
import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||
|
||||
import { getExtensionDestFolder } from "./get-extension-dest-folder";
|
||||
|
||||
const getExtensionDestFolderInjectable = getInjectable({
|
||||
id: "get-extension-dest-folder",
|
||||
|
||||
instantiate: (di) =>
|
||||
getExtensionDestFolder({
|
||||
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default getExtensionDestFolderInjectable;
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { ExtensionDiscovery } from "../../../../../extensions/extension-discovery/extension-discovery";
|
||||
import { sanitizeExtensionName } from "../../../../../extensions/lens-extension";
|
||||
import path from "path";
|
||||
|
||||
interface Dependencies {
|
||||
extensionDiscovery: ExtensionDiscovery;
|
||||
}
|
||||
|
||||
export const getExtensionDestFolder =
|
||||
({ extensionDiscovery }: Dependencies) =>
|
||||
(name: string) =>
|
||||
path.join(extensionDiscovery.localFolderPath, sanitizeExtensionName(name));
|
||||
@@ -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>;
|
||||
}
|
||||
+36
-27
@@ -2,33 +2,38 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate";
|
||||
import type { Disposer } from "../../../../../common/utils";
|
||||
import { extractTar, noop } from "../../../../../common/utils";
|
||||
import { extensionDisplayName } from "../../../../../extensions/lens-extension";
|
||||
import logger from "../../../../../main/logger";
|
||||
import type { ExtensionLoader } from "../../../../../extensions/extension-loader";
|
||||
import { Notifications } from "../../../notifications";
|
||||
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
||||
import { 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 { ExtensionInstallationStateStore } from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
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";
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
getExtensionDestFolder: (name: string) => string;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
export type UnpackExtension = (request: InstallRequestValidated, disposeDownloading?: Disposer) => Promise<void>;
|
||||
|
||||
export const unpackExtension =
|
||||
({
|
||||
extensionLoader,
|
||||
getExtensionDestFolder,
|
||||
extensionInstallationStateStore,
|
||||
}: Dependencies) =>
|
||||
async (request: InstallRequestValidated, disposeDownloading?: Disposer) => {
|
||||
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,
|
||||
@@ -73,13 +78,13 @@ export const unpackExtension =
|
||||
// Enable installed extensions by default.
|
||||
extensionLoader.setIsEnabled(id, true);
|
||||
|
||||
Notifications.ok(
|
||||
showInfoNotification((
|
||||
<p>
|
||||
{"Extension "}
|
||||
<b>{displayName}</b>
|
||||
{" successfully installed!"}
|
||||
</p>,
|
||||
);
|
||||
</p>
|
||||
));
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
@@ -87,14 +92,14 @@ export const unpackExtension =
|
||||
`[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`,
|
||||
{ error },
|
||||
);
|
||||
Notifications.error(
|
||||
showErrorNotification((
|
||||
<p>
|
||||
{"Installing extension "}
|
||||
<b>{displayName}</b>
|
||||
{" has failed: "}
|
||||
<em>{message}</em>
|
||||
</p>,
|
||||
);
|
||||
</p>
|
||||
));
|
||||
} finally {
|
||||
// Remove install state once finished
|
||||
extensionInstallationStateStore.clearInstalling(id);
|
||||
@@ -104,3 +109,7 @@ export const unpackExtension =
|
||||
fse.unlink(tempFile).catch(noop);
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default unpackExtensionInjectable;
|
||||
-24
@@ -1,24 +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 { unpackExtension } from "./unpack-extension";
|
||||
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import getExtensionDestFolderInjectable
|
||||
from "../get-extension-dest-folder/get-extension-dest-folder.injectable";
|
||||
import extensionInstallationStateStoreInjectable
|
||||
from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
|
||||
const unpackExtensionInjectable = getInjectable({
|
||||
id: "unpack-extension",
|
||||
|
||||
instantiate: (di) =>
|
||||
unpackExtension({
|
||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||
getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default unpackExtensionInjectable;
|
||||
+7
-11
@@ -2,27 +2,23 @@
|
||||
* 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/*"
|
||||
const firstFile = tarFiles[0];
|
||||
|
||||
if (!firstFile) {
|
||||
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
|
||||
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
|
||||
}
|
||||
|
||||
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`);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import attemptInstallInjectable from "./attempt-install/attempt-install.injectable";
|
||||
import path from "path";
|
||||
import readFileNotifyInjectable from "./read-file-notify/read-file-notify.injectable";
|
||||
|
||||
export type AttemptInstalls = (filePaths: string[]) => Promise<void>;
|
||||
|
||||
const attemptInstallsInjectable = getInjectable({
|
||||
id: "attempt-installs",
|
||||
|
||||
instantiate: (di): AttemptInstalls => {
|
||||
const attemptInstall = di.inject(attemptInstallInjectable);
|
||||
const readFileNotify = di.inject(readFileNotifyInjectable);
|
||||
|
||||
return async (filePaths) => {
|
||||
await Promise.allSettled(
|
||||
filePaths.map(async filePath => {
|
||||
const data = await readFileNotify(filePath);
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
return attemptInstall({
|
||||
fileName: path.basename(filePath),
|
||||
data,
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default attemptInstallsInjectable;
|
||||
@@ -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 { attemptInstalls } from "./attempt-installs";
|
||||
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
|
||||
|
||||
const attemptInstallsInjectable = getInjectable({
|
||||
id: "attempt-installs",
|
||||
|
||||
instantiate: (di) =>
|
||||
attemptInstalls({
|
||||
attemptInstall: di.inject(attemptInstallInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default attemptInstallsInjectable;
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { readFileNotify } from "../read-file-notify/read-file-notify";
|
||||
import path from "path";
|
||||
import type { InstallRequest } from "../attempt-install/install-request";
|
||||
|
||||
interface Dependencies {
|
||||
attemptInstall: (request: InstallRequest) => Promise<void>;
|
||||
}
|
||||
|
||||
export const attemptInstalls =
|
||||
({ attemptInstall }: Dependencies) =>
|
||||
async (filePaths: string[]): Promise<void> => {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
promises.push(
|
||||
attemptInstall({
|
||||
fileName: path.basename(filePath),
|
||||
dataP: readFileNotify(filePath),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
};
|
||||
@@ -28,23 +28,24 @@ import enableExtensionInjectable from "./enable-extension/enable-extension.injec
|
||||
import disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
|
||||
import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable";
|
||||
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable";
|
||||
import installFromInputInjectable from "./install-from-input/install-from-input.injectable";
|
||||
import type { InstallExtensionFromInput } from "./install-extension-from-input.injectable";
|
||||
import installExtensionFromInputInjectable from "./install-extension-from-input.injectable";
|
||||
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
|
||||
import type { LensExtensionId } from "../../../extensions/lens-extension";
|
||||
import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable";
|
||||
import type { InstallOnDrop } from "./install-on-drop.injectable";
|
||||
import installOnDropInjectable from "./install-on-drop.injectable";
|
||||
import { supportedExtensionFormats } from "./supported-extension-formats";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import type { InstallFromInput } from "./install-from-input/install-from-input";
|
||||
|
||||
interface Dependencies {
|
||||
userExtensions: IComputedValue<InstalledExtension[]>;
|
||||
enableExtension: (id: LensExtensionId) => void;
|
||||
disableExtension: (id: LensExtensionId) => void;
|
||||
confirmUninstallExtension: ConfirmUninstallExtension;
|
||||
installFromInput: InstallFromInput;
|
||||
installExtensionFromInput: InstallExtensionFromInput;
|
||||
installFromSelectFileDialog: () => Promise<void>;
|
||||
installOnDrop: (files: File[]) => Promise<void>;
|
||||
installOnDrop: InstallOnDrop;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
@@ -107,7 +108,7 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
|
||||
<Install
|
||||
supportedFormats={supportedExtensionFormats}
|
||||
onChange={value => (this.installPath = value)}
|
||||
installFromInput={() => this.props.installFromInput(this.installPath)}
|
||||
installFromInput={() => this.props.installExtensionFromInput(this.installPath)}
|
||||
installFromSelectFileDialog={this.props.installFromSelectFileDialog}
|
||||
installPath={this.installPath}
|
||||
/>
|
||||
@@ -131,7 +132,7 @@ export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
|
||||
enableExtension: di.inject(enableExtensionInjectable),
|
||||
disableExtension: di.inject(disableExtensionInjectable),
|
||||
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
|
||||
installFromInput: di.inject(installFromInputInjectable),
|
||||
installExtensionFromInput: di.inject(installExtensionFromInputInjectable),
|
||||
installOnDrop: di.inject(installOnDropInjectable),
|
||||
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
import type { ExtendableDisposer } from "../../../common/utils";
|
||||
import { InputValidators } from "../input";
|
||||
import { getMessageFromError } from "./get-message-from-error/get-message-from-error";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import attemptInstallInjectable from "./attempt-install/attempt-install.injectable";
|
||||
import attemptInstallByInfoInjectable from "./attempt-install-by-info.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import readFileNotifyInjectable from "./read-file-notify/read-file-notify.injectable";
|
||||
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
||||
import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import downloadBinaryInjectable from "../../../common/fetch/download-binary.injectable";
|
||||
import { withTimeout } from "../../../common/fetch/timeout-controller";
|
||||
|
||||
export type InstallExtensionFromInput = (input: string) => Promise<void>;
|
||||
|
||||
const installExtensionFromInputInjectable = getInjectable({
|
||||
id: "install-extension-from-input",
|
||||
|
||||
instantiate: (di): InstallExtensionFromInput => {
|
||||
const attemptInstall = di.inject(attemptInstallInjectable);
|
||||
const attemptInstallByInfo = di.inject(attemptInstallByInfoInjectable);
|
||||
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||
const readFileNotify = di.inject(readFileNotifyInjectable);
|
||||
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const downloadBinary = di.inject(downloadBinaryInjectable);
|
||||
|
||||
return async (input) => {
|
||||
let disposer: ExtendableDisposer | undefined = undefined;
|
||||
|
||||
try {
|
||||
// fixme: improve error messages for non-tar-file URLs
|
||||
if (InputValidators.isUrl.validate(input)) {
|
||||
// install via url
|
||||
disposer = extensionInstallationStateStore.startPreInstall();
|
||||
const { 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, data: result.response }, disposer);
|
||||
}
|
||||
|
||||
try {
|
||||
await InputValidators.isPath.validate(input);
|
||||
|
||||
// install from system path
|
||||
const fileName = getBasenameOfPath(input);
|
||||
const data = await readFileNotify(input);
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await attemptInstall({ fileName, data });
|
||||
} catch (error) {
|
||||
const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input);
|
||||
|
||||
if (extNameCaptures) {
|
||||
const { name, version } = extNameCaptures;
|
||||
|
||||
return await attemptInstallByInfo({ name, version });
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unknown format of input: ${input}`);
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(`[EXTENSION-INSTALL]: installation has failed: ${message}`, { error, installPath: input });
|
||||
showErrorNotification((
|
||||
<p>
|
||||
{"Installation has failed: "}
|
||||
<b>{message}</b>
|
||||
</p>
|
||||
));
|
||||
} finally {
|
||||
disposer?.();
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default installExtensionFromInputInjectable;
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
|
||||
import { installFromInput } from "./install-from-input";
|
||||
import attemptInstallByInfoInjectable from "../attempt-install-by-info.injectable";
|
||||
import extensionInstallationStateStoreInjectable
|
||||
from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
|
||||
const installFromInputInjectable = getInjectable({
|
||||
id: "install-extension-from-input",
|
||||
|
||||
instantiate: (di) =>
|
||||
installFromInput({
|
||||
attemptInstall: di.inject(attemptInstallInjectable),
|
||||
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default installFromInputInjectable;
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { ExtendableDisposer } from "../../../../common/utils";
|
||||
import { downloadFile } from "../../../../common/utils";
|
||||
import { InputValidators } from "../../input";
|
||||
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
||||
import logger from "../../../../main/logger";
|
||||
import { Notifications } from "../../notifications";
|
||||
import path from "path";
|
||||
import React from "react";
|
||||
import { readFileNotify } from "../read-file-notify/read-file-notify";
|
||||
import type { InstallRequest } from "../attempt-install/install-request";
|
||||
import type { ExtensionInfo } from "../attempt-install-by-info.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
|
||||
export type InstallFromInput = (input: string) => Promise<void>;
|
||||
|
||||
interface Dependencies {
|
||||
attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => Promise<void>;
|
||||
attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise<void>;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
export const installFromInput = ({
|
||||
attemptInstall,
|
||||
attemptInstallByInfo,
|
||||
extensionInstallationStateStore,
|
||||
}: Dependencies): InstallFromInput => (
|
||||
async (input) => {
|
||||
let disposer: ExtendableDisposer | undefined = undefined;
|
||||
|
||||
try {
|
||||
// fixme: improve error messages for non-tar-file URLs
|
||||
if (InputValidators.isUrl.validate(input)) {
|
||||
// install via url
|
||||
disposer = extensionInstallationStateStore.startPreInstall();
|
||||
const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 });
|
||||
const fileName = path.basename(input);
|
||||
|
||||
return await attemptInstall({ fileName, dataP: promise }, disposer);
|
||||
}
|
||||
|
||||
try {
|
||||
await InputValidators.isPath.validate(input);
|
||||
|
||||
// install from system path
|
||||
const fileName = path.basename(input);
|
||||
|
||||
return await attemptInstall({ fileName, dataP: readFileNotify(input) });
|
||||
} catch (error) {
|
||||
const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input);
|
||||
|
||||
if (extNameCaptures) {
|
||||
const { name, version } = extNameCaptures;
|
||||
|
||||
return await attemptInstallByInfo({ name, version });
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unknown format of input: ${input}`);
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(`[EXTENSION-INSTALL]: installation has failed: ${message}`, { error, installPath: input });
|
||||
Notifications.error((
|
||||
<p>
|
||||
{"Installation has failed: "}
|
||||
<b>{message}</b>
|
||||
</p>
|
||||
));
|
||||
} finally {
|
||||
disposer?.();
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable";
|
||||
import loggerInjectable from "../../../../common/logger.injectable";
|
||||
import readFileBufferInjectable from "../../../../common/fs/read-file-buffer.injectable";
|
||||
|
||||
export type ReadFileNotify = (filePath: string, showError?: boolean) => Promise<Buffer | null>;
|
||||
|
||||
const readFileNotifyInjectable = getInjectable({
|
||||
id: "read-file-notify",
|
||||
instantiate: (di): ReadFileNotify => {
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const readFileBuffer = di.inject(readFileBufferInjectable);
|
||||
|
||||
return async (filePath, showError = true) => {
|
||||
try {
|
||||
return await readFileBuffer(filePath);
|
||||
} catch (error) {
|
||||
if (showError) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(`[EXTENSION-INSTALL]: preloading ${filePath} has failed: ${message}`, { error });
|
||||
showErrorNotification(`Error while reading "${filePath}": ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default readFileNotifyInjectable;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import fse from "fs-extra";
|
||||
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
||||
import logger from "../../../../main/logger";
|
||||
import { Notifications } from "../../notifications";
|
||||
|
||||
export const readFileNotify = async (filePath: string, showError = true): Promise<Buffer | null> => {
|
||||
try {
|
||||
return await fse.readFile(filePath);
|
||||
} catch (error) {
|
||||
if (showError) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(`[EXTENSION-INSTALL]: preloading ${filePath} has failed: ${message}`, { error });
|
||||
Notifications.error(`Error while reading "${filePath}": ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
+21
-14
@@ -4,30 +4,37 @@
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||
import callForHelmChartReadmeInjectable from "./readme/call-for-helm-chart-readme.injectable";
|
||||
import helmChartDetailsVersionSelectionInjectable from "./versions/helm-chart-details-version-selection.injectable";
|
||||
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import requestHelmChartReadmeInjectable from "../../../../common/k8s-api/endpoints/helm-charts.api/request-readme.injectable";
|
||||
|
||||
const readmeOfSelectedHelmChartInjectable = getInjectable({
|
||||
id: "readme-of-selected-helm-chart",
|
||||
|
||||
instantiate: (di, chart: HelmChart) => {
|
||||
const selection = di.inject(helmChartDetailsVersionSelectionInjectable, chart);
|
||||
const callForHelmChartReadme = di.inject(callForHelmChartReadmeInjectable);
|
||||
const selection = di.inject(
|
||||
helmChartDetailsVersionSelectionInjectable,
|
||||
chart,
|
||||
);
|
||||
const requestHelmChartReadme = di.inject(requestHelmChartReadmeInjectable);
|
||||
|
||||
return asyncComputed(async () => {
|
||||
const chartVersion = selection.value.get();
|
||||
return asyncComputed({
|
||||
getValueFromObservedPromise: async () => {
|
||||
const chartVersion = selection.value.get();
|
||||
|
||||
if (!chartVersion) {
|
||||
return "";
|
||||
}
|
||||
if (!chartVersion) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return await callForHelmChartReadme(
|
||||
chartVersion.getRepository(),
|
||||
chartVersion.getName(),
|
||||
chartVersion.getVersion(),
|
||||
);
|
||||
}, "");
|
||||
return await requestHelmChartReadme(
|
||||
chartVersion.getRepository(),
|
||||
chartVersion.getName(),
|
||||
chartVersion.getVersion(),
|
||||
);
|
||||
},
|
||||
|
||||
valueWhenPending: "",
|
||||
});
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
|
||||
-29
@@ -1,29 +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 { getChartDetails } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
|
||||
export type CallForHelmChartReadme = (
|
||||
repo: string,
|
||||
name: string,
|
||||
version: string,
|
||||
) => Promise<string>;
|
||||
|
||||
const callForHelmChartReadmeInjectable = getInjectable({
|
||||
id: "call-for-helm-chart-readme",
|
||||
|
||||
instantiate:
|
||||
(): CallForHelmChartReadme =>
|
||||
async (repository: string, name: string, version: string) => {
|
||||
// TODO: Dismantle wrong abstraction
|
||||
const details = await getChartDetails(repository, name, { version });
|
||||
|
||||
return details.readme;
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForHelmChartReadmeInjectable;
|
||||
+10
-7
@@ -4,20 +4,23 @@
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||
import callForHelmChartVersionsInjectable from "./versions/call-for-helm-chart-versions.injectable";
|
||||
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import requestHelmChartVersionsInjectable from "../../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
|
||||
|
||||
const versionsOfSelectedHelmChartInjectable = getInjectable({
|
||||
id: "versions-of-selected-helm-chart",
|
||||
|
||||
instantiate: (di, chart: HelmChart) => {
|
||||
const callForHelmChartVersions = di.inject(callForHelmChartVersionsInjectable);
|
||||
|
||||
return asyncComputed(
|
||||
async () =>
|
||||
await callForHelmChartVersions(chart.getRepository(), chart.getName()),
|
||||
[],
|
||||
const requestHelmChartVersions = di.inject(
|
||||
requestHelmChartVersionsInjectable,
|
||||
);
|
||||
|
||||
return asyncComputed({
|
||||
getValueFromObservedPromise: async () =>
|
||||
await requestHelmChartVersions(chart.getRepository(), chart.getName()),
|
||||
|
||||
valueWhenPending: [],
|
||||
});
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
|
||||
-28
@@ -1,28 +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 type { HelmChart } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { getChartDetails } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
|
||||
export type CallForHelmChartVersions = (
|
||||
repo: string,
|
||||
name: string
|
||||
) => Promise<HelmChart[]>;
|
||||
|
||||
const callForHelmChartVersionsInjectable = getInjectable({
|
||||
id: "call-for-helm-chart-versions",
|
||||
|
||||
instantiate:
|
||||
(): CallForHelmChartVersions => async (repository: string, name: string) => {
|
||||
// TODO: Dismantle wrong abstraction
|
||||
const details = await getChartDetails(repository, name);
|
||||
|
||||
return details.versions;
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForHelmChartVersionsInjectable;
|
||||
@@ -1,17 +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 type { GetChartDetailsOptions, IHelmChartDetails } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { getChartDetails } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
|
||||
export type GetChartDetails = (repo: string, name: string, opts?: GetChartDetailsOptions) => Promise<IHelmChartDetails>;
|
||||
|
||||
const getChartDetailsInjectable = getInjectable({
|
||||
id: "get-chart-details",
|
||||
instantiate: (): GetChartDetails => getChartDetails,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default getChartDetailsInjectable;
|
||||
@@ -117,16 +117,10 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
|
||||
</a>
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Maintainers" className="maintainers">
|
||||
{selectedChart.getMaintainers().map(({ name, email, url }) => (
|
||||
<a
|
||||
key={name}
|
||||
href={url || `mailto:${email}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
))}
|
||||
{selectedChart.getMaintainers()
|
||||
.map(({ name, email }) => (
|
||||
<li key={name}>{`${name}<${email}>`}</li>
|
||||
))}
|
||||
</DrawerItem>
|
||||
{selectedChart.getKeywords().length > 0 && (
|
||||
<DrawerItem name="Keywords" labelsOnly>
|
||||
|
||||
@@ -1,13 +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 { HelmChartStore } from "./helm-chart.store";
|
||||
|
||||
const helmChartStoreInjectable = getInjectable({
|
||||
id: "helm-chart-store",
|
||||
instantiate: () => new HelmChartStore(),
|
||||
});
|
||||
|
||||
export default helmChartStoreInjectable;
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import semver from "semver";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { autoBind, sortCompareChartVersions } from "../../utils";
|
||||
import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { getChartDetails, listCharts } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { ItemStore } from "../../../common/item.store";
|
||||
import flatten from "lodash/flatten";
|
||||
|
||||
export interface ChartVersion {
|
||||
repo: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export class HelmChartStore extends ItemStore<HelmChart> {
|
||||
@observable versions = observable.map<string, ChartVersion[]>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
async loadAll() {
|
||||
try {
|
||||
const res = await this.loadItems(() => listCharts());
|
||||
|
||||
this.failedLoading = false;
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
this.failedLoading = true;
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getByName(name: string, repo?: string) {
|
||||
if (typeof repo !== "string") {
|
||||
/**
|
||||
* FIXME:
|
||||
* This is here because in strict mode `getByName` MUST be 100% compatiable if called in the
|
||||
* situation where it is only a "ItemStore"
|
||||
*/
|
||||
throw new TypeError("repo must be provided");
|
||||
}
|
||||
|
||||
return this.items.find(chart => chart.getName() === name && chart.getRepository() === repo);
|
||||
}
|
||||
|
||||
protected sortVersions = (versions: ChartVersion[]) => {
|
||||
return versions
|
||||
.map(chartVersion => ({ ...chartVersion, __version: semver.coerce(chartVersion.version, { loose: true }) }))
|
||||
.sort(sortCompareChartVersions)
|
||||
.map(({ __version, ...chartVersion }) => chartVersion);
|
||||
};
|
||||
|
||||
async getVersions(chartName: string, force?: boolean): Promise<ChartVersion[]> {
|
||||
const versions = this.versions.get(chartName);
|
||||
|
||||
if (versions && !force) {
|
||||
return versions;
|
||||
}
|
||||
|
||||
const loadVersions = async (repo: string) => {
|
||||
const { versions } = await getChartDetails(repo, chartName);
|
||||
|
||||
return versions.map(chart => ({
|
||||
repo,
|
||||
version: chart.getVersion(),
|
||||
}));
|
||||
};
|
||||
|
||||
if (!this.isLoaded) {
|
||||
await this.loadAll();
|
||||
}
|
||||
const repos = this.items
|
||||
.filter(chart => chart.getName() === chartName)
|
||||
.map(chart => chart.getRepository());
|
||||
|
||||
const newVersions = await Promise.all(repos.map(loadVersions))
|
||||
.then(flatten)
|
||||
.then(this.sortVersions);
|
||||
|
||||
this.versions.set(chartName, newVersions);
|
||||
|
||||
return newVersions;
|
||||
}
|
||||
|
||||
reset() {
|
||||
super.reset();
|
||||
this.versions.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Not supported
|
||||
*/
|
||||
removeItems(): Promise<void> {
|
||||
throw new Error("removeItems is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
export const helmChartStore = new HelmChartStore();
|
||||
@@ -7,7 +7,6 @@ import "./helm-charts.scss";
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { helmChartStore } from "./helm-chart.store";
|
||||
import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { HelmChartDetails } from "./helm-chart-details";
|
||||
import { ItemListLayout } from "../item-object-list/list-layout";
|
||||
@@ -21,6 +20,7 @@ import navigateToHelmChartsInjectable from "../../../common/front-end-routing/ro
|
||||
import { HelmChartIcon } from "./icon";
|
||||
import helmChartsInjectable from "./helm-charts/helm-charts.injectable";
|
||||
import selectedHelmChartInjectable from "./helm-charts/selected-helm-chart.injectable";
|
||||
import { noop } from "lodash";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@@ -35,9 +35,7 @@ interface Dependencies {
|
||||
chartName: IComputedValue<string>;
|
||||
repo: IComputedValue<string>;
|
||||
};
|
||||
|
||||
navigateToHelmCharts: NavigateToHelmCharts;
|
||||
|
||||
charts: IAsyncComputed<HelmChart[]>;
|
||||
selectedChart: IComputedValue<HelmChart | undefined>;
|
||||
}
|
||||
@@ -69,17 +67,31 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { charts } = this.props;
|
||||
const selectedChart = this.props.selectedChart.get();
|
||||
|
||||
return (
|
||||
<SiblingsInTabLayout>
|
||||
<div data-testid="page-for-helm-charts" style={{ display: "none" }}/>
|
||||
|
||||
<ItemListLayout
|
||||
<ItemListLayout<HelmChart, false>
|
||||
isConfigurable
|
||||
tableId="helm_charts"
|
||||
className="HelmCharts"
|
||||
store={helmChartStore}
|
||||
store={{
|
||||
get isLoaded() {
|
||||
return !charts.pending.get();
|
||||
},
|
||||
failedLoading: false,
|
||||
getTotalCount: () => charts.value.get().length,
|
||||
isSelected: (item) => item === selectedChart,
|
||||
toggleSelection: noop,
|
||||
isSelectedAll: () => false,
|
||||
toggleSelectionAll: () => false,
|
||||
pickOnlySelected: () => [],
|
||||
removeSelectedItems: async () => {},
|
||||
}}
|
||||
preloadStores={false}
|
||||
getItems={() => this.props.charts.value.get()}
|
||||
isSelectable={false}
|
||||
sortingCallbacks={{
|
||||
@@ -132,16 +144,12 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
|
||||
}
|
||||
}
|
||||
|
||||
export const HelmCharts = withInjectables<Dependencies>(
|
||||
NonInjectedHelmCharts,
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
routeParameters: di.inject(helmChartsRouteParametersInjectable),
|
||||
navigateToHelmCharts: di.inject(navigateToHelmChartsInjectable),
|
||||
charts: di.inject(helmChartsInjectable),
|
||||
selectedChart: di.inject(selectedHelmChartInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
export const HelmCharts = withInjectables<Dependencies>(NonInjectedHelmCharts, {
|
||||
getProps: (di) => ({
|
||||
routeParameters: di.inject(helmChartsRouteParametersInjectable),
|
||||
navigateToHelmCharts: di.inject(navigateToHelmChartsInjectable),
|
||||
charts: di.inject(helmChartsInjectable),
|
||||
selectedChart: di.inject(selectedHelmChartInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,17 +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 type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { listCharts } from "../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
|
||||
export type CallForHelmCharts = () => Promise<HelmChart[]>;
|
||||
|
||||
const callForHelmChartsInjectable = getInjectable({
|
||||
id: "call-for-helm-charts",
|
||||
instantiate: (): CallForHelmCharts => async () => await listCharts(),
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForHelmChartsInjectable;
|
||||
@@ -4,15 +4,18 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||
import callForHelmChartsInjectable from "./call-for-helm-charts.injectable";
|
||||
import requestHelmChartsInjectable from "../../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
|
||||
|
||||
const helmChartsInjectable = getInjectable({
|
||||
id: "helm-charts",
|
||||
|
||||
instantiate: (di) => {
|
||||
const callForHelmCharts = di.inject(callForHelmChartsInjectable);
|
||||
const requestHelmCharts = di.inject(requestHelmChartsInjectable);
|
||||
|
||||
return asyncComputed(async () => await callForHelmCharts(), []);
|
||||
return asyncComputed({
|
||||
getValueFromObservedPromise: requestHelmCharts,
|
||||
valueWhenPending: [],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import requestHelmChartVersionsInjectable from "../../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
|
||||
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { sortBySemverVersion } from "../../../utils";
|
||||
import type { HelmChartVersion } from "./versions";
|
||||
|
||||
/**
|
||||
* @param release The release to try and figure out what chart versions match for
|
||||
* @param charts The list of possible helm charts that `release` could be of
|
||||
*/
|
||||
export type RequestVersionsOfHelmChart = (release: HelmRelease, charts: HelmChart[]) => Promise<HelmChartVersion[]>;
|
||||
|
||||
const requestVersionsOfHelmChartInjectable = getInjectable({
|
||||
id: "request-versions-of-helm-chart-for-helm-release-from-list-of-charts",
|
||||
instantiate: (di): RequestVersionsOfHelmChart => {
|
||||
const requestHelmChartVersions = di.inject(requestHelmChartVersionsInjectable);
|
||||
|
||||
return async (release, charts) => {
|
||||
const rawVersions = await Promise.all((
|
||||
charts
|
||||
.filter(chart => chart.getName() === release.getChart())
|
||||
.map(chart => chart.getRepository())
|
||||
.map(repo => requestHelmChartVersions(repo, release.getChart()))
|
||||
));
|
||||
|
||||
return sortBySemverVersion(rawVersions.flat());
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestVersionsOfHelmChartInjectable;
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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 requestHelmChartsInjectable from "../../../../common/k8s-api/endpoints/helm-charts.api/request-charts.injectable";
|
||||
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import requestVersionsOfHelmChartInjectable from "./request-versions-of-chart-for-release.injectable";
|
||||
import type { HelmChartVersion } from "./versions";
|
||||
|
||||
export type RequestVersionsOfHelmChartFor = (release: HelmRelease) => Promise<HelmChartVersion[]>;
|
||||
|
||||
const requestVersionsOfHelmChartForInjectable = getInjectable({
|
||||
id: "request-versions-of-helm-chart-for",
|
||||
instantiate: (di): RequestVersionsOfHelmChartFor => {
|
||||
const requestHelmCharts = di.inject(requestHelmChartsInjectable);
|
||||
const requestVersionsOfHelmChart = di.inject(requestVersionsOfHelmChartInjectable);
|
||||
|
||||
return async (release) => {
|
||||
const charts = await requestHelmCharts();
|
||||
|
||||
return requestVersionsOfHelmChart(release, charts);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestVersionsOfHelmChartForInjectable;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||
import { when } from "mobx";
|
||||
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import helmChartsInjectable from "./helm-charts.injectable";
|
||||
import requestVersionsOfHelmChartInjectable from "./request-versions-of-chart-for-release.injectable";
|
||||
|
||||
const helmChartVersionsInjectable = getInjectable({
|
||||
id: "helm-chart-versions-loader",
|
||||
|
||||
instantiate: (di, release) => {
|
||||
const helmCharts = di.inject(helmChartsInjectable);
|
||||
const requestVersionsOfHelmChart = di.inject(requestVersionsOfHelmChartInjectable);
|
||||
|
||||
return asyncComputed({
|
||||
getValueFromObservedPromise: async () => {
|
||||
await when(() => !helmCharts.pending.get());
|
||||
|
||||
return requestVersionsOfHelmChart(release, helmCharts.value.get());
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, release: HelmRelease) => release.getName(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default helmChartVersionsInjectable;
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A type for the possible versions that a helm release was made from
|
||||
*/
|
||||
export interface HelmChartVersion {
|
||||
repo: string;
|
||||
version: string;
|
||||
}
|
||||
Loaded 100 of 1328 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user