mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Making apiBase injectable (#6022)
* Making apiBase injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all of Helm functions to be DI Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make PortForward's use of apiBase fully injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all metric requests to be injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace resource applier with injectables Signed-off-by: Sebastian Malton <sebastian@malton.name> * Switch KubeJsonApi.forCluster to be injectable but do not use Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert the rest of shell sessions to be DI-ed - This is a prerequesit for using the new createKubeJsonApiForClusterInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Use new createKubeJsonApiForClusterInjectable for openNodeShellSession Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make KubeconfigDialog injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove jest-fetch-mock and make fetch injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests with new global override Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add new injectable for create KubeJsonApi and JsonApi instances Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix showing-details-for-helm-release behavioural tests - Remove HelmChartStore in favour of all injectables - Create a model for UpgradeChartDockTab Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix show details and updating helm releases tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix residual typing issues related to metrics Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix crash on load due to circular dependency Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix create resource tab not working Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove legacy apiBase global Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce and use isDebuggingInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce and use windowLocationInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove global legacy apiKube Signed-off-by: Sebastian Malton <sebastian@malton.name> * Improve injectable filenames compared to the injectables inside Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove modifying input in requestActivePortForwardInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce and use get(Milli)SecondsFromUnixEpochInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Switch to non-reactive way of gettting possible helm release versions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix typo Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix bug in KubeApi constructor Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all KubeApi related tests to use asyncFn Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix unit tests after introducing new injectables that have side effects Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix bad rebase causing tests to fail Signed-off-by: Sebastian Malton <sebastian@malton.name> * Improve expects for multiple field values Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix crash will looking up api refs Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix breaking change on KubeApi.list Signed-off-by: Sebastian Malton <sebastian@malton.name> * Better fix for formatting urls Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove injectable for time since we should just use useMockTime Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add happy path behavioural tests for upgrade chart tab Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove debug message Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix showing-details-for-helm-release tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix installing-helm-chart-from-new-tab tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests relating to hosted cluster id Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots to recent changes in master Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Reupdated upgrade chart new tab test snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix flakiness in unit test when using <Animated> Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix flakiness and improve tests for DeleteClusterDialog Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix kubeconfig-sync tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix <Extensions> tests by removing mockFs and making everything injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix build issues Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix getElectronAppPathInjectable override not returning absolute paths - Also fixes the listing-active-helm-repos-in-prefs tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace all uses of getAbsolutePath with joinPaths as it is more correct and less confusing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix opening application window tests by making override properly absolute Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots relating no longer using getAbsolutePath Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix and add behavioural tests for RenderDelay Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix extension discovery tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix test flakiness because of path side effects, propagate uses to as many places Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix extension-discovery tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add global override to fix some tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Rewrite and fix implementation of KubeconfigManager and its tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests by global override pathExists Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix unit tests failing on windows by using injectable verions of path functions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Attempt to fix test timeout by using runInAction Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix setupIpcMainHandlers usage Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> Co-authored-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
367 files changed
+16701
-5302
No files matched your search
@@ -10,7 +10,6 @@ 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";
|
||||
@@ -25,6 +24,8 @@ 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 { GetDirnameOfPath } from "../../../common/path/get-dirname.injectable";
|
||||
import getDirnameOfPathInjectable from "../../../common/path/get-dirname.injectable";
|
||||
|
||||
interface Option {
|
||||
config: KubeConfig;
|
||||
@@ -34,6 +35,7 @@ interface Option {
|
||||
interface Dependencies {
|
||||
getCustomKubeConfigDirectory: (directoryName: string) => string;
|
||||
navigateToCatalog: NavigateToCatalog;
|
||||
getDirnameOfPath: GetDirnameOfPath;
|
||||
}
|
||||
|
||||
function getContexts(config: KubeConfig): Map<string, Option> {
|
||||
@@ -90,7 +92,7 @@ class NonInjectedAddCluster extends React.Component<Dependencies> {
|
||||
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)`);
|
||||
@@ -155,10 +157,8 @@ 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),
|
||||
}),
|
||||
});
|
||||
@@ -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),
|
||||
}),
|
||||
});
|
||||
@@ -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 (
|
||||
|
||||
@@ -5,14 +5,11 @@
|
||||
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import fse from "fs-extra";
|
||||
import React from "react";
|
||||
import type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery";
|
||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
import { ConfirmDialog } from "../../confirm-dialog";
|
||||
import { Extensions } from "../extensions";
|
||||
import mockFs from "mock-fs";
|
||||
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import type { DiRender } from "../../test-utils/renderFor";
|
||||
@@ -20,18 +17,15 @@ import { renderFor } from "../../test-utils/renderFor";
|
||||
import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import directoryForDownloadsInjectable from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable";
|
||||
import getConfigurationFileModelInjectable from "../../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import assert from "assert";
|
||||
import type { InstallFromInput } from "../install-from-input/install-from-input";
|
||||
import installFromInputInjectable from "../install-from-input/install-from-input.injectable";
|
||||
import type { InstallExtensionFromInput } from "../install-extension-from-input.injectable";
|
||||
import installExtensionFromInputInjectable from "../install-extension-from-input.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import extensionInstallationStateStoreInjectable from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import { observable, when } from "mobx";
|
||||
import type { DeleteFile } from "../../../../common/fs/delete-file.injectable";
|
||||
import deleteFileInjectable from "../../../../common/fs/delete-file.injectable";
|
||||
|
||||
mockWindow();
|
||||
|
||||
jest.setTimeout(30000);
|
||||
jest.mock("fs-extra");
|
||||
jest.mock("../../notifications");
|
||||
|
||||
jest.mock("../../../../common/utils/downloadFile", () => ({
|
||||
@@ -52,9 +46,10 @@ jest.mock("../../../../common/utils/tar");
|
||||
describe("Extensions", () => {
|
||||
let extensionLoader: ExtensionLoader;
|
||||
let extensionDiscovery: ExtensionDiscovery;
|
||||
let installFromInput: jest.MockedFunction<InstallFromInput>;
|
||||
let installExtensionFromInput: jest.MockedFunction<InstallExtensionFromInput>;
|
||||
let extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
let render: DiRender;
|
||||
let deleteFileMock: jest.MockedFunction<DeleteFile>;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
@@ -62,17 +57,13 @@ describe("Extensions", () => {
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
di.override(directoryForDownloadsInjectable, () => "some-directory-for-downloads");
|
||||
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
|
||||
mockFs({
|
||||
"some-directory-for-user-data": {},
|
||||
});
|
||||
|
||||
render = renderFor(di);
|
||||
|
||||
installFromInput = jest.fn();
|
||||
installExtensionFromInput = jest.fn();
|
||||
di.override(installExtensionFromInputInjectable, () => installExtensionFromInput);
|
||||
|
||||
di.override(installFromInputInjectable, () => installFromInput);
|
||||
deleteFileMock = jest.fn();
|
||||
di.override(deleteFileInjectable, () => deleteFileMock);
|
||||
|
||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
@@ -95,10 +86,6 @@ describe("Extensions", () => {
|
||||
extensionDiscovery.uninstallExtension = jest.fn(() => Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("disables uninstall and disable buttons while uninstalling", async () => {
|
||||
extensionDiscovery.isLoaded = true;
|
||||
|
||||
@@ -138,8 +125,8 @@ describe("Extensions", () => {
|
||||
|
||||
const resolveInstall = observable.box(false);
|
||||
|
||||
(fse.unlink as jest.MockedFunction<typeof fse.unlink>).mockReturnValue(Promise.resolve());
|
||||
installFromInput.mockImplementation(async (input) => {
|
||||
deleteFileMock.mockReturnValue(Promise.resolve());
|
||||
installExtensionFromInput.mockImplementation(async (input) => {
|
||||
expect(input).toBe("https://test.extensionurl/package.tgz");
|
||||
|
||||
const clear = extensionInstallationStateStore.startPreInstall();
|
||||
|
||||
@@ -2,22 +2,18 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { ExtendableDisposer } from "../../../common/utils";
|
||||
import { downloadFile, downloadJson } from "../../../common/utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import React from "react";
|
||||
import path from "path";
|
||||
import { SemVer } from "semver";
|
||||
import URLParse from "url-parse";
|
||||
import type { InstallRequest } from "./attempt-install/install-request";
|
||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import type { Confirm } from "../confirm-dialog/confirm.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import attemptInstallInjectable from "./attempt-install/attempt-install.injectable";
|
||||
import getBaseRegistryUrlInjectable from "./get-base-registry-url/get-base-registry-url.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import confirmInjectable from "../confirm-dialog/confirm.injectable";
|
||||
import { reduce } from "lodash";
|
||||
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
||||
|
||||
export interface ExtensionInfo {
|
||||
name: string;
|
||||
@@ -27,126 +23,113 @@ export interface ExtensionInfo {
|
||||
|
||||
export type AttemptInstallByInfo = (info: ExtensionInfo) => Promise<void>;
|
||||
|
||||
interface Dependencies {
|
||||
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>;
|
||||
getBaseRegistryUrl: () => Promise<string>;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
confirm: Confirm;
|
||||
}
|
||||
const attemptInstallByInfoInjectable = getInjectable({
|
||||
id: "attempt-install-by-info",
|
||||
instantiate: (di): AttemptInstallByInfo => {
|
||||
const attemptInstall = di.inject(attemptInstallInjectable);
|
||||
const getBaseRegistryUrl = di.inject(getBaseRegistryUrlInjectable);
|
||||
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||
const confirm = di.inject(confirmInjectable);
|
||||
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
|
||||
|
||||
const attemptInstallByInfo = ({
|
||||
attemptInstall,
|
||||
getBaseRegistryUrl,
|
||||
extensionInstallationStateStore,
|
||||
confirm,
|
||||
}: Dependencies): AttemptInstallByInfo => (
|
||||
async (info) => {
|
||||
const { name, version, requireConfirmation = false } = info;
|
||||
const disposer = extensionInstallationStateStore.startPreInstall();
|
||||
const baseUrl = await getBaseRegistryUrl();
|
||||
const registryUrl = new URLParse(baseUrl).set("pathname", name).toString();
|
||||
let json: any;
|
||||
let finalVersion = version;
|
||||
return async (info) => {
|
||||
const { name, version, requireConfirmation = false } = info;
|
||||
const disposer = extensionInstallationStateStore.startPreInstall();
|
||||
const baseUrl = await getBaseRegistryUrl();
|
||||
const registryUrl = new URLParse(baseUrl).set("pathname", name).toString();
|
||||
let json: any;
|
||||
let finalVersion = version;
|
||||
|
||||
try {
|
||||
json = await downloadJson({ url: registryUrl }).promise;
|
||||
try {
|
||||
json = await downloadJson({ url: registryUrl }).promise;
|
||||
|
||||
if (!json || json.error || typeof json.versions !== "object" || !json.versions) {
|
||||
const message = json?.error ? `: ${json.error}` : "";
|
||||
if (!json || json.error || typeof json.versions !== "object" || !json.versions) {
|
||||
const message = json?.error ? `: ${json.error}` : "";
|
||||
|
||||
Notifications.error(`Failed to get registry information for that extension${message}`);
|
||||
|
||||
return disposer();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
// assume invalid JSON
|
||||
console.warn("Set registry has invalid json", { url: baseUrl }, error);
|
||||
Notifications.error("Failed to get valid registry information for that extension. Registry did not return valid JSON");
|
||||
} else {
|
||||
console.error("Failed to download registry information", error);
|
||||
Notifications.error(`Failed to get valid registry information for that extension. ${error}`);
|
||||
}
|
||||
|
||||
return disposer();
|
||||
}
|
||||
|
||||
if (version) {
|
||||
if (!json.versions[version]) {
|
||||
if (json["dist-tags"][version]) {
|
||||
finalVersion = json["dist-tags"][version];
|
||||
} else {
|
||||
Notifications.error((
|
||||
<p>
|
||||
{"The "}
|
||||
<em>{name}</em>
|
||||
{" extension does not have a version or tag "}
|
||||
<code>{version}</code>
|
||||
.
|
||||
</p>
|
||||
));
|
||||
Notifications.error(`Failed to get registry information for that extension${message}`);
|
||||
|
||||
return disposer();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
// assume invalid JSON
|
||||
console.warn("Set registry has invalid json", { url: baseUrl }, error);
|
||||
Notifications.error("Failed to get valid registry information for that extension. Registry did not return valid JSON");
|
||||
} else {
|
||||
console.error("Failed to download registry information", error);
|
||||
Notifications.error(`Failed to get valid registry information for that extension. ${error}`);
|
||||
}
|
||||
|
||||
return disposer();
|
||||
}
|
||||
} else {
|
||||
const versions = Object.keys(json.versions)
|
||||
.map(version => new SemVer(version, { loose: true }))
|
||||
|
||||
if (version) {
|
||||
if (!json.versions[version]) {
|
||||
if (json["dist-tags"][version]) {
|
||||
finalVersion = json["dist-tags"][version];
|
||||
} else {
|
||||
Notifications.error((
|
||||
<p>
|
||||
{"The "}
|
||||
<em>{name}</em>
|
||||
{" extension does not have a version or tag "}
|
||||
<code>{version}</code>
|
||||
.
|
||||
</p>
|
||||
));
|
||||
|
||||
return disposer();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const versions = Object.keys(json.versions)
|
||||
.map(version => new SemVer(version, { loose: true }))
|
||||
// ignore pre-releases for auto picking the version
|
||||
.filter(version => version.prerelease.length === 0);
|
||||
.filter(version => version.prerelease.length === 0);
|
||||
|
||||
const latestVersion = reduce(versions, (prev, curr) => prev.compareMain(curr) === -1 ? curr : prev);
|
||||
const latestVersion = reduce(versions, (prev, curr) => prev.compareMain(curr) === -1 ? curr : prev);
|
||||
|
||||
if (!latestVersion) {
|
||||
console.error("No versions supplied for that extension", { name });
|
||||
Notifications.error(`No versions found for ${name}`);
|
||||
if (!latestVersion) {
|
||||
console.error("No versions supplied for that extension", { name });
|
||||
Notifications.error(`No versions found for ${name}`);
|
||||
|
||||
return disposer();
|
||||
return disposer();
|
||||
}
|
||||
|
||||
finalVersion = latestVersion.format();
|
||||
}
|
||||
|
||||
finalVersion = latestVersion.format();
|
||||
}
|
||||
if (requireConfirmation) {
|
||||
const proceed = await confirm({
|
||||
message: (
|
||||
<p>
|
||||
Are you sure you want to install
|
||||
{" "}
|
||||
<b>
|
||||
{name}
|
||||
@
|
||||
{finalVersion}
|
||||
</b>
|
||||
?
|
||||
</p>
|
||||
),
|
||||
labelCancel: "Cancel",
|
||||
labelOk: "Install",
|
||||
});
|
||||
|
||||
if (requireConfirmation) {
|
||||
const proceed = await confirm({
|
||||
message: (
|
||||
<p>
|
||||
Are you sure you want to install
|
||||
{" "}
|
||||
<b>
|
||||
{name}
|
||||
@
|
||||
{finalVersion}
|
||||
</b>
|
||||
?
|
||||
</p>
|
||||
),
|
||||
labelCancel: "Cancel",
|
||||
labelOk: "Install",
|
||||
});
|
||||
|
||||
if (!proceed) {
|
||||
return disposer();
|
||||
if (!proceed) {
|
||||
return disposer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const url = json.versions[finalVersion!].dist.tarball;
|
||||
const fileName = path.basename(url);
|
||||
const { promise: dataP } = downloadFile({ url, timeout: 10 * 60 * 1000 });
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const url = json.versions[finalVersion!].dist.tarball;
|
||||
const fileName = getBasenameOfPath(url);
|
||||
const { promise: dataP } = downloadFile({ url, timeout: 10 * 60 * 1000 });
|
||||
|
||||
return attemptInstall({ fileName, dataP }, disposer);
|
||||
}
|
||||
);
|
||||
|
||||
const attemptInstallByInfoInjectable = getInjectable({
|
||||
id: "attempt-install-by-info",
|
||||
instantiate: (di) => attemptInstallByInfo({
|
||||
attemptInstall: di.inject(attemptInstallInjectable),
|
||||
getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
confirm: di.inject(confirmInjectable),
|
||||
}),
|
||||
return attemptInstall({ fileName, dataP }, disposer);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default attemptInstallByInfoInjectable;
|
||||
@@ -2,12 +2,8 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type {
|
||||
Disposer,
|
||||
ExtendableDisposer } from "../../../../common/utils";
|
||||
import {
|
||||
disposer,
|
||||
} from "../../../../common/utils";
|
||||
import type { ExtendableDisposer } from "../../../../common/utils";
|
||||
import { disposer } from "../../../../common/utils";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { Button } from "../../button";
|
||||
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
@@ -15,29 +11,19 @@ import type { LensExtensionId } from "../../../../extensions/lens-extension";
|
||||
import React from "react";
|
||||
import { remove as removeDir } from "fs-extra";
|
||||
import { shell } from "electron";
|
||||
import type { InstallRequestValidated } from "./create-temp-files-and-validate/create-temp-files-and-validate";
|
||||
import type { InstallRequest } from "./install-request";
|
||||
import type {
|
||||
ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import {
|
||||
ExtensionInstallationState,
|
||||
} from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import { ExtensionInstallationState } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import type { UnpackExtension } from "./unpack-extension/unpack-extension.injectable";
|
||||
import type { CreateTempFilesAndValidate } from "./create-temp-files-and-validate/create-temp-files-and-validate.injectable";
|
||||
import type { GetExtensionDestFolder } from "./get-extension-dest-folder/get-extension-dest-folder.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
uninstallExtension: (id: LensExtensionId) => Promise<boolean>;
|
||||
|
||||
unpackExtension: (
|
||||
request: InstallRequestValidated,
|
||||
disposeDownloading: Disposer,
|
||||
) => Promise<void>;
|
||||
|
||||
createTempFilesAndValidate: (
|
||||
installRequest: InstallRequest,
|
||||
) => Promise<InstallRequestValidated | null>;
|
||||
|
||||
getExtensionDestFolder: (name: string) => string;
|
||||
|
||||
unpackExtension: UnpackExtension;
|
||||
createTempFilesAndValidate: CreateTempFilesAndValidate;
|
||||
getExtensionDestFolder: GetExtensionDestFolder;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
@@ -51,6 +37,8 @@ export const attemptInstall =
|
||||
extensionInstallationStateStore,
|
||||
}: Dependencies) =>
|
||||
async (request: InstallRequest, d?: ExtendableDisposer): Promise<void> => {
|
||||
console.log("Attempting to install extension");
|
||||
|
||||
const dispose = disposer(
|
||||
extensionInstallationStateStore.startPreInstall(),
|
||||
d,
|
||||
|
||||
+91
-5
@@ -3,16 +3,102 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { createTempFilesAndValidate } from "./create-temp-files-and-validate";
|
||||
import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import React from "react";
|
||||
import type { LensExtensionId, LensExtensionManifest } from "../../../../../extensions/lens-extension";
|
||||
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
||||
import type { InstallRequest } from "../install-request";
|
||||
import { validatePackage } from "../validate-package/validate-package";
|
||||
import joinPathsInjectable from "../../../../../common/path/join-paths.injectable";
|
||||
import tempDirectoryPathInjectable from "../../../../../common/os/temp-directory-path.injectable";
|
||||
import ensureDirInjectable from "../../../../../common/fs/ensure-dir.injectable";
|
||||
import writeFileInjectable from "../../../../../common/fs/write-file.injectable";
|
||||
import loggerInjectable from "../../../../../common/logger.injectable";
|
||||
import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable";
|
||||
|
||||
export interface InstallRequestValidated {
|
||||
fileName: string;
|
||||
data: Buffer;
|
||||
id: LensExtensionId;
|
||||
manifest: LensExtensionManifest;
|
||||
tempFile: string; // temp system path to packed extension for unpacking
|
||||
}
|
||||
|
||||
export type CreateTempFilesAndValidate = (req: InstallRequest) => Promise<InstallRequestValidated | null>;
|
||||
|
||||
const createTempFilesAndValidateInjectable = getInjectable({
|
||||
id: "create-temp-files-and-validate",
|
||||
|
||||
instantiate: (di) =>
|
||||
createTempFilesAndValidate({
|
||||
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||
}),
|
||||
instantiate: (di) => {
|
||||
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const tempDirectoryPath = di.inject(tempDirectoryPathInjectable);
|
||||
const ensureDir = di.inject(ensureDirInjectable);
|
||||
const writeFile = di.inject(writeFileInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
|
||||
const baseTempExtensionsDirectory = joinPaths(tempDirectoryPath, "lens-extensions");
|
||||
const getExtensionPackageTemp = (fileName: string) => joinPaths(baseTempExtensionsDirectory, fileName);
|
||||
|
||||
return async ({
|
||||
fileName,
|
||||
dataP,
|
||||
}: InstallRequest): Promise<InstallRequestValidated | null> => {
|
||||
// copy files to temp
|
||||
await ensureDir(baseTempExtensionsDirectory);
|
||||
|
||||
// validate packages
|
||||
const tempFile = getExtensionPackageTemp(fileName);
|
||||
|
||||
try {
|
||||
const data = await dataP;
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await writeFile(tempFile, data);
|
||||
logger.info("validating package", tempFile);
|
||||
const manifest = await validatePackage(tempFile);
|
||||
const id = joinPaths(
|
||||
extensionDiscovery.nodeModulesPath,
|
||||
manifest.name,
|
||||
"package.json",
|
||||
);
|
||||
|
||||
return {
|
||||
fileName,
|
||||
data,
|
||||
manifest,
|
||||
tempFile,
|
||||
id,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(
|
||||
`[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`,
|
||||
{ error },
|
||||
);
|
||||
showErrorNotification(
|
||||
<div className="flex column gaps">
|
||||
<p>
|
||||
{"Installing "}
|
||||
<em>{fileName}</em>
|
||||
{" has failed, skipping."}
|
||||
</p>
|
||||
<p>
|
||||
{"Reason: "}
|
||||
<em>{message}</em>
|
||||
</p>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default createTempFilesAndValidateInjectable;
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { validatePackage } from "../validate-package/validate-package";
|
||||
import type { ExtensionDiscovery } from "../../../../../extensions/extension-discovery/extension-discovery";
|
||||
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
||||
import logger from "../../../../../main/logger";
|
||||
import { Notifications } from "../../../notifications";
|
||||
import path from "path";
|
||||
import fse from "fs-extra";
|
||||
import React from "react";
|
||||
import os from "os";
|
||||
import type {
|
||||
LensExtensionId,
|
||||
LensExtensionManifest,
|
||||
} from "../../../../../extensions/lens-extension";
|
||||
import type { InstallRequest } from "../install-request";
|
||||
|
||||
export interface InstallRequestValidated {
|
||||
fileName: string;
|
||||
data: Buffer;
|
||||
id: LensExtensionId;
|
||||
manifest: LensExtensionManifest;
|
||||
tempFile: string; // temp system path to packed extension for unpacking
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
extensionDiscovery: ExtensionDiscovery;
|
||||
}
|
||||
|
||||
export const createTempFilesAndValidate =
|
||||
({ extensionDiscovery }: Dependencies) =>
|
||||
async ({
|
||||
fileName,
|
||||
dataP,
|
||||
}: InstallRequest): Promise<InstallRequestValidated | null> => {
|
||||
// copy files to temp
|
||||
await fse.ensureDir(getExtensionPackageTemp());
|
||||
|
||||
// validate packages
|
||||
const tempFile = getExtensionPackageTemp(fileName);
|
||||
|
||||
try {
|
||||
const data = await dataP;
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await fse.writeFile(tempFile, data);
|
||||
const manifest = await validatePackage(tempFile);
|
||||
const id = path.join(
|
||||
extensionDiscovery.nodeModulesPath,
|
||||
manifest.name,
|
||||
"package.json",
|
||||
);
|
||||
|
||||
return {
|
||||
fileName,
|
||||
data,
|
||||
manifest,
|
||||
tempFile,
|
||||
id,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(
|
||||
`[EXTENSION-INSTALLATION]: installing ${fileName} has failed: ${message}`,
|
||||
{ error },
|
||||
);
|
||||
Notifications.error(
|
||||
<div className="flex column gaps">
|
||||
<p>
|
||||
{"Installing "}
|
||||
<em>{fileName}</em>
|
||||
{" has failed, skipping."}
|
||||
</p>
|
||||
<p>
|
||||
{"Reason: "}
|
||||
<em>{message}</em>
|
||||
</p>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
function getExtensionPackageTemp(fileName = "") {
|
||||
return path.join(os.tmpdir(), "lens-extensions", fileName);
|
||||
}
|
||||
+9
-6
@@ -3,18 +3,21 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
|
||||
import joinPathsInjectable from "../../../../../common/path/join-paths.injectable";
|
||||
import extensionDiscoveryInjectable from "../../../../../extensions/extension-discovery/extension-discovery.injectable";
|
||||
import { sanitizeExtensionName } from "../../../../../extensions/lens-extension";
|
||||
|
||||
import { getExtensionDestFolder } from "./get-extension-dest-folder";
|
||||
export type GetExtensionDestFolder = (extensionName: string) => string;
|
||||
|
||||
const getExtensionDestFolderInjectable = getInjectable({
|
||||
id: "get-extension-dest-folder",
|
||||
|
||||
instantiate: (di) =>
|
||||
getExtensionDestFolder({
|
||||
extensionDiscovery: di.inject(extensionDiscoveryInjectable),
|
||||
}),
|
||||
instantiate: (di): GetExtensionDestFolder => {
|
||||
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
|
||||
return (name) => joinPaths(extensionDiscovery.localFolderPath, sanitizeExtensionName(name));
|
||||
},
|
||||
});
|
||||
|
||||
export default getExtensionDestFolderInjectable;
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { ExtensionDiscovery } from "../../../../../extensions/extension-discovery/extension-discovery";
|
||||
import { sanitizeExtensionName } from "../../../../../extensions/lens-extension";
|
||||
import path from "path";
|
||||
|
||||
interface Dependencies {
|
||||
extensionDiscovery: ExtensionDiscovery;
|
||||
}
|
||||
|
||||
export const getExtensionDestFolder =
|
||||
({ extensionDiscovery }: Dependencies) =>
|
||||
(name: string) =>
|
||||
path.join(extensionDiscovery.localFolderPath, sanitizeExtensionName(name));
|
||||
+117
-11
@@ -2,23 +2,129 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { unpackExtension } from "./unpack-extension";
|
||||
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";
|
||||
import getExtensionDestFolderInjectable
|
||||
from "../get-extension-dest-folder/get-extension-dest-folder.injectable";
|
||||
import extensionInstallationStateStoreInjectable
|
||||
from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import getExtensionDestFolderInjectable from "../get-extension-dest-folder/get-extension-dest-folder.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate.injectable";
|
||||
import type { Disposer } from "../../../../utils";
|
||||
import { noop } from "../../../../utils";
|
||||
import { extensionDisplayName } from "../../../../../extensions/lens-extension";
|
||||
import joinPathsInjectable from "../../../../../common/path/join-paths.injectable";
|
||||
import loggerInjectable from "../../../../../common/logger.injectable";
|
||||
import { when } from "mobx";
|
||||
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
||||
import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable";
|
||||
import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable";
|
||||
import getDirnameOfPathInjectable from "../../../../../common/path/get-dirname.injectable";
|
||||
import getBasenameOfPathInjectable from "../../../../../common/path/get-basename.injectable";
|
||||
import extractTarInjectable from "../../../../../common/fs/extract-tar.injectable";
|
||||
import ensureDirInjectable from "../../../../../common/fs/ensure-dir.injectable";
|
||||
import removePathInjectable from "../../../../../common/fs/remove-path.injectable";
|
||||
import deleteFileInjectable from "../../../../../common/fs/delete-file.injectable";
|
||||
import readDirectoryInjectable from "../../../../../common/fs/read-directory.injectable";
|
||||
import moveInjectable from "../../../../../common/fs/move.injectable";
|
||||
|
||||
export type UnpackExtension = (request: InstallRequestValidated, disposeDownloading?: Disposer) => Promise<void>;
|
||||
|
||||
const unpackExtensionInjectable = getInjectable({
|
||||
id: "unpack-extension",
|
||||
|
||||
instantiate: (di) =>
|
||||
unpackExtension({
|
||||
extensionLoader: di.inject(extensionLoaderInjectable),
|
||||
getExtensionDestFolder: di.inject(getExtensionDestFolderInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
}),
|
||||
instantiate: (di): UnpackExtension => {
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
const getExtensionDestFolder = di.inject(getExtensionDestFolderInjectable);
|
||||
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
|
||||
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const showOkNotification = di.inject(showSuccessNotificationInjectable);
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
const extractTar = di.inject(extractTarInjectable);
|
||||
const ensureDir = di.inject(ensureDirInjectable);
|
||||
const removePath = di.inject(removePathInjectable);
|
||||
const deleteFile = di.inject(deleteFileInjectable);
|
||||
const readDirectory = di.inject(readDirectoryInjectable);
|
||||
const move = di.inject(moveInjectable);
|
||||
|
||||
return async (request, disposeDownloading) => {
|
||||
const {
|
||||
id,
|
||||
fileName,
|
||||
tempFile,
|
||||
manifest: { name, version },
|
||||
} = request;
|
||||
|
||||
extensionInstallationStateStore.setInstalling(id);
|
||||
disposeDownloading?.();
|
||||
|
||||
const displayName = extensionDisplayName(name, version);
|
||||
const extensionFolder = getExtensionDestFolder(name);
|
||||
const unpackingTempFolder = joinPaths(
|
||||
getDirnameOfPath(tempFile),
|
||||
`${getBasenameOfPath(tempFile)}-unpacked`,
|
||||
);
|
||||
|
||||
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
||||
|
||||
try {
|
||||
// extract to temp folder first
|
||||
await removePath(unpackingTempFolder).catch(noop);
|
||||
await ensureDir(unpackingTempFolder);
|
||||
await extractTar(tempFile, { cwd: unpackingTempFolder });
|
||||
|
||||
// move contents to extensions folder
|
||||
const unpackedFiles = await readDirectory(unpackingTempFolder);
|
||||
let unpackedRootFolder = unpackingTempFolder;
|
||||
|
||||
if (unpackedFiles.length === 1) {
|
||||
// check if %extension.tgz was packed with single top folder,
|
||||
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
|
||||
unpackedRootFolder = joinPaths(unpackingTempFolder, unpackedFiles[0]);
|
||||
}
|
||||
|
||||
await ensureDir(extensionFolder);
|
||||
await move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
||||
|
||||
// wait for the loader has actually install it
|
||||
await when(() => extensionLoader.userExtensions.has(id));
|
||||
|
||||
// Enable installed extensions by default.
|
||||
extensionLoader.setIsEnabled(id, true);
|
||||
|
||||
showOkNotification(
|
||||
<p>
|
||||
{"Extension "}
|
||||
<b>{displayName}</b>
|
||||
{" successfully installed!"}
|
||||
</p>,
|
||||
);
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(
|
||||
`[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`,
|
||||
{ error },
|
||||
);
|
||||
showErrorNotification(
|
||||
<p>
|
||||
{"Installing extension "}
|
||||
<b>{displayName}</b>
|
||||
{" has failed: "}
|
||||
<em>{message}</em>
|
||||
</p>,
|
||||
);
|
||||
} finally {
|
||||
// Remove install state once finished
|
||||
extensionInstallationStateStore.clearInstalling(id);
|
||||
|
||||
// clean up
|
||||
removePath(unpackingTempFolder).catch(noop);
|
||||
deleteFile(tempFile).catch(noop);
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default unpackExtensionInjectable;
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate";
|
||||
import type { Disposer } from "../../../../../common/utils";
|
||||
import { extractTar, noop } from "../../../../../common/utils";
|
||||
import { extensionDisplayName } from "../../../../../extensions/lens-extension";
|
||||
import logger from "../../../../../main/logger";
|
||||
import type { ExtensionLoader } from "../../../../../extensions/extension-loader";
|
||||
import { Notifications } from "../../../notifications";
|
||||
import { getMessageFromError } from "../../get-message-from-error/get-message-from-error";
|
||||
import path from "path";
|
||||
import fse from "fs-extra";
|
||||
import { when } from "mobx";
|
||||
import React from "react";
|
||||
import type { ExtensionInstallationStateStore } from "../../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
getExtensionDestFolder: (name: string) => string;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
export const unpackExtension =
|
||||
({
|
||||
extensionLoader,
|
||||
getExtensionDestFolder,
|
||||
extensionInstallationStateStore,
|
||||
}: Dependencies) =>
|
||||
async (request: InstallRequestValidated, disposeDownloading?: Disposer) => {
|
||||
const {
|
||||
id,
|
||||
fileName,
|
||||
tempFile,
|
||||
manifest: { name, version },
|
||||
} = request;
|
||||
|
||||
extensionInstallationStateStore.setInstalling(id);
|
||||
disposeDownloading?.();
|
||||
|
||||
const displayName = extensionDisplayName(name, version);
|
||||
const extensionFolder = getExtensionDestFolder(name);
|
||||
const unpackingTempFolder = path.join(
|
||||
path.dirname(tempFile),
|
||||
`${path.basename(tempFile)}-unpacked`,
|
||||
);
|
||||
|
||||
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
||||
|
||||
try {
|
||||
// extract to temp folder first
|
||||
await fse.remove(unpackingTempFolder).catch(noop);
|
||||
await fse.ensureDir(unpackingTempFolder);
|
||||
await extractTar(tempFile, { cwd: unpackingTempFolder });
|
||||
|
||||
// move contents to extensions folder
|
||||
const unpackedFiles = await fse.readdir(unpackingTempFolder);
|
||||
let unpackedRootFolder = unpackingTempFolder;
|
||||
|
||||
if (unpackedFiles.length === 1) {
|
||||
// check if %extension.tgz was packed with single top folder,
|
||||
// e.g. "npm pack %ext_name" downloads file with "package" root folder within tarball
|
||||
unpackedRootFolder = path.join(unpackingTempFolder, unpackedFiles[0]);
|
||||
}
|
||||
|
||||
await fse.ensureDir(extensionFolder);
|
||||
await fse.move(unpackedRootFolder, extensionFolder, { overwrite: true });
|
||||
|
||||
// wait for the loader has actually install it
|
||||
await when(() => extensionLoader.userExtensions.has(id));
|
||||
|
||||
// Enable installed extensions by default.
|
||||
extensionLoader.setIsEnabled(id, true);
|
||||
|
||||
Notifications.ok(
|
||||
<p>
|
||||
{"Extension "}
|
||||
<b>{displayName}</b>
|
||||
{" successfully installed!"}
|
||||
</p>,
|
||||
);
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(
|
||||
`[EXTENSION-INSTALLATION]: installing ${request.fileName} has failed: ${message}`,
|
||||
{ error },
|
||||
);
|
||||
Notifications.error(
|
||||
<p>
|
||||
{"Installing extension "}
|
||||
<b>{displayName}</b>
|
||||
{" has failed: "}
|
||||
<em>{message}</em>
|
||||
</p>,
|
||||
);
|
||||
} finally {
|
||||
// Remove install state once finished
|
||||
extensionInstallationStateStore.clearInstalling(id);
|
||||
|
||||
// clean up
|
||||
fse.remove(unpackingTempFolder).catch(noop);
|
||||
fse.unlink(tempFile).catch(noop);
|
||||
}
|
||||
};
|
||||
+18
-5
@@ -3,16 +3,29 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { attemptInstalls } from "./attempt-installs";
|
||||
import getBasenameOfPathInjectable from "../../../../common/path/get-basename.injectable";
|
||||
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
|
||||
import readFileNotifyInjectable from "../read-file-notify/read-file-notify.injectable";
|
||||
|
||||
export type AttemptInstalls = (filePaths: string[]) => Promise<void>;
|
||||
|
||||
const attemptInstallsInjectable = getInjectable({
|
||||
id: "attempt-installs",
|
||||
|
||||
instantiate: (di) =>
|
||||
attemptInstalls({
|
||||
attemptInstall: di.inject(attemptInstallInjectable),
|
||||
}),
|
||||
instantiate: (di): AttemptInstalls => {
|
||||
const attemptInstall = di.inject(attemptInstallInjectable);
|
||||
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
|
||||
const readFileNotify = di.inject(readFileNotifyInjectable);
|
||||
|
||||
return async (filePaths) => {
|
||||
await Promise.allSettled(filePaths.map(filePath => (
|
||||
attemptInstall({
|
||||
fileName: getBasenameOfPath(filePath),
|
||||
dataP: readFileNotify(filePath),
|
||||
})
|
||||
)));
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default attemptInstallsInjectable;
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { readFileNotify } from "../read-file-notify/read-file-notify";
|
||||
import path from "path";
|
||||
import type { InstallRequest } from "../attempt-install/install-request";
|
||||
|
||||
interface Dependencies {
|
||||
attemptInstall: (request: InstallRequest) => Promise<void>;
|
||||
}
|
||||
|
||||
export const attemptInstalls =
|
||||
({ attemptInstall }: Dependencies) =>
|
||||
async (filePaths: string[]): Promise<void> => {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
promises.push(
|
||||
attemptInstall({
|
||||
fileName: path.basename(filePath),
|
||||
dataP: readFileNotify(filePath),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
};
|
||||
@@ -28,21 +28,21 @@ import enableExtensionInjectable from "./enable-extension/enable-extension.injec
|
||||
import disableExtensionInjectable from "./disable-extension/disable-extension.injectable";
|
||||
import type { ConfirmUninstallExtension } from "./confirm-uninstall-extension.injectable";
|
||||
import confirmUninstallExtensionInjectable from "./confirm-uninstall-extension.injectable";
|
||||
import installFromInputInjectable from "./install-from-input/install-from-input.injectable";
|
||||
import type { InstallExtensionFromInput } from "./install-extension-from-input.injectable";
|
||||
import installExtensionFromInputInjectable from "./install-extension-from-input.injectable";
|
||||
import installFromSelectFileDialogInjectable from "./install-from-select-file-dialog.injectable";
|
||||
import type { LensExtensionId } from "../../../extensions/lens-extension";
|
||||
import installOnDropInjectable from "./install-on-drop/install-on-drop.injectable";
|
||||
import { supportedExtensionFormats } from "./supported-extension-formats";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
import type { InstallFromInput } from "./install-from-input/install-from-input";
|
||||
|
||||
interface Dependencies {
|
||||
userExtensions: IComputedValue<InstalledExtension[]>;
|
||||
enableExtension: (id: LensExtensionId) => void;
|
||||
disableExtension: (id: LensExtensionId) => void;
|
||||
confirmUninstallExtension: ConfirmUninstallExtension;
|
||||
installFromInput: InstallFromInput;
|
||||
installExtensionFromInput: InstallExtensionFromInput;
|
||||
installFromSelectFileDialog: () => Promise<void>;
|
||||
installOnDrop: (files: File[]) => Promise<void>;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
@@ -107,7 +107,7 @@ class NonInjectedExtensions extends React.Component<Dependencies> {
|
||||
<Install
|
||||
supportedFormats={supportedExtensionFormats}
|
||||
onChange={value => (this.installPath = value)}
|
||||
installFromInput={() => this.props.installFromInput(this.installPath)}
|
||||
installFromInput={() => this.props.installExtensionFromInput(this.installPath)}
|
||||
installFromSelectFileDialog={this.props.installFromSelectFileDialog}
|
||||
installPath={this.installPath}
|
||||
/>
|
||||
@@ -131,7 +131,7 @@ export const Extensions = withInjectables<Dependencies>(NonInjectedExtensions, {
|
||||
enableExtension: di.inject(enableExtensionInjectable),
|
||||
disableExtension: di.inject(disableExtensionInjectable),
|
||||
confirmUninstallExtension: di.inject(confirmUninstallExtensionInjectable),
|
||||
installFromInput: di.inject(installFromInputInjectable),
|
||||
installExtensionFromInput: di.inject(installExtensionFromInputInjectable),
|
||||
installOnDrop: di.inject(installOnDropInjectable),
|
||||
installFromSelectFileDialog: di.inject(installFromSelectFileDialogInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
import type { ExtendableDisposer } from "../../../common/utils";
|
||||
import { downloadFile } from "../../../common/utils";
|
||||
import { InputValidators } from "../input";
|
||||
import { getMessageFromError } from "./get-message-from-error/get-message-from-error";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import attemptInstallInjectable from "./attempt-install/attempt-install.injectable";
|
||||
import attemptInstallByInfoInjectable from "./attempt-install-by-info.injectable";
|
||||
import extensionInstallationStateStoreInjectable from "../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
import readFileNotifyInjectable from "./read-file-notify/read-file-notify.injectable";
|
||||
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
|
||||
import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
|
||||
export type InstallExtensionFromInput = (input: string) => Promise<void>;
|
||||
|
||||
const installExtensionFromInputInjectable = getInjectable({
|
||||
id: "install-extension-from-input",
|
||||
|
||||
instantiate: (di): InstallExtensionFromInput => {
|
||||
const attemptInstall = di.inject(attemptInstallInjectable);
|
||||
const attemptInstallByInfo = di.inject(attemptInstallByInfoInjectable);
|
||||
const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable);
|
||||
const readFileNotify = di.inject(readFileNotifyInjectable);
|
||||
const getBasenameOfPath = di.inject(getBasenameOfPathInjectable);
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
return async (input) => {
|
||||
let disposer: ExtendableDisposer | undefined = undefined;
|
||||
|
||||
try {
|
||||
// fixme: improve error messages for non-tar-file URLs
|
||||
if (InputValidators.isUrl.validate(input)) {
|
||||
// install via url
|
||||
disposer = extensionInstallationStateStore.startPreInstall();
|
||||
const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 });
|
||||
const fileName = getBasenameOfPath(input);
|
||||
|
||||
return await attemptInstall({ fileName, dataP: promise }, disposer);
|
||||
}
|
||||
|
||||
try {
|
||||
await InputValidators.isPath.validate(input);
|
||||
|
||||
// install from system path
|
||||
const fileName = getBasenameOfPath(input);
|
||||
|
||||
return await attemptInstall({ fileName, dataP: readFileNotify(input) });
|
||||
} catch (error) {
|
||||
const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input);
|
||||
|
||||
if (extNameCaptures) {
|
||||
const { name, version } = extNameCaptures;
|
||||
|
||||
return await attemptInstallByInfo({ name, version });
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unknown format of input: ${input}`);
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(`[EXTENSION-INSTALL]: installation has failed: ${message}`, { error, installPath: input });
|
||||
showErrorNotification((
|
||||
<p>
|
||||
{"Installation has failed: "}
|
||||
<b>{message}</b>
|
||||
</p>
|
||||
));
|
||||
} finally {
|
||||
disposer?.();
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default installExtensionFromInputInjectable;
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
|
||||
import { installFromInput } from "./install-from-input";
|
||||
import attemptInstallByInfoInjectable from "../attempt-install-by-info.injectable";
|
||||
import extensionInstallationStateStoreInjectable
|
||||
from "../../../../extensions/extension-installation-state-store/extension-installation-state-store.injectable";
|
||||
|
||||
const installFromInputInjectable = getInjectable({
|
||||
id: "install-extension-from-input",
|
||||
|
||||
instantiate: (di) =>
|
||||
installFromInput({
|
||||
attemptInstall: di.inject(attemptInstallInjectable),
|
||||
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
|
||||
extensionInstallationStateStore: di.inject(extensionInstallationStateStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
export default installFromInputInjectable;
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { ExtendableDisposer } from "../../../../common/utils";
|
||||
import { downloadFile } from "../../../../common/utils";
|
||||
import { InputValidators } from "../../input";
|
||||
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
||||
import logger from "../../../../main/logger";
|
||||
import { Notifications } from "../../notifications";
|
||||
import path from "path";
|
||||
import React from "react";
|
||||
import { readFileNotify } from "../read-file-notify/read-file-notify";
|
||||
import type { InstallRequest } from "../attempt-install/install-request";
|
||||
import type { ExtensionInfo } from "../attempt-install-by-info.injectable";
|
||||
import type { ExtensionInstallationStateStore } from "../../../../extensions/extension-installation-state-store/extension-installation-state-store";
|
||||
|
||||
export type InstallFromInput = (input: string) => Promise<void>;
|
||||
|
||||
interface Dependencies {
|
||||
attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => Promise<void>;
|
||||
attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise<void>;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
}
|
||||
|
||||
export const installFromInput = ({
|
||||
attemptInstall,
|
||||
attemptInstallByInfo,
|
||||
extensionInstallationStateStore,
|
||||
}: Dependencies): InstallFromInput => (
|
||||
async (input) => {
|
||||
let disposer: ExtendableDisposer | undefined = undefined;
|
||||
|
||||
try {
|
||||
// fixme: improve error messages for non-tar-file URLs
|
||||
if (InputValidators.isUrl.validate(input)) {
|
||||
// install via url
|
||||
disposer = extensionInstallationStateStore.startPreInstall();
|
||||
const { promise } = downloadFile({ url: input, timeout: 10 * 60 * 1000 });
|
||||
const fileName = path.basename(input);
|
||||
|
||||
return await attemptInstall({ fileName, dataP: promise }, disposer);
|
||||
}
|
||||
|
||||
try {
|
||||
await InputValidators.isPath.validate(input);
|
||||
|
||||
// install from system path
|
||||
const fileName = path.basename(input);
|
||||
|
||||
return await attemptInstall({ fileName, dataP: readFileNotify(input) });
|
||||
} catch (error) {
|
||||
const extNameCaptures = InputValidators.isExtensionNameInstallRegex.captures(input);
|
||||
|
||||
if (extNameCaptures) {
|
||||
const { name, version } = extNameCaptures;
|
||||
|
||||
return await attemptInstallByInfo({ name, version });
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unknown format of input: ${input}`);
|
||||
} catch (error) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(`[EXTENSION-INSTALL]: installation has failed: ${message}`, { error, installPath: input });
|
||||
Notifications.error((
|
||||
<p>
|
||||
{"Installation has failed: "}
|
||||
<b>{message}</b>
|
||||
</p>
|
||||
));
|
||||
} finally {
|
||||
disposer?.();
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable";
|
||||
import loggerInjectable from "../../../../common/logger.injectable";
|
||||
import readFileBufferInjectable from "../../../../common/fs/read-file-buffer.injectable";
|
||||
|
||||
export type ReadFileNotify = (filePath: string, showError?: boolean) => Promise<Buffer | null>;
|
||||
|
||||
const readFileNotifyInjectable = getInjectable({
|
||||
id: "read-file-notify",
|
||||
instantiate: (di): ReadFileNotify => {
|
||||
const showErrorNotification = di.inject(showErrorNotificationInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const readFileBuffer = di.inject(readFileBufferInjectable);
|
||||
|
||||
return async (filePath, showError = true) => {
|
||||
try {
|
||||
return await readFileBuffer(filePath);
|
||||
} catch (error) {
|
||||
if (showError) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(`[EXTENSION-INSTALL]: preloading ${filePath} has failed: ${message}`, { error });
|
||||
showErrorNotification(`Error while reading "${filePath}": ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default readFileNotifyInjectable;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import fse from "fs-extra";
|
||||
import { getMessageFromError } from "../get-message-from-error/get-message-from-error";
|
||||
import logger from "../../../../main/logger";
|
||||
import { Notifications } from "../../notifications";
|
||||
|
||||
export const readFileNotify = async (filePath: string, showError = true): Promise<Buffer | null> => {
|
||||
try {
|
||||
return await fse.readFile(filePath);
|
||||
} catch (error) {
|
||||
if (showError) {
|
||||
const message = getMessageFromError(error);
|
||||
|
||||
logger.info(`[EXTENSION-INSTALL]: preloading ${filePath} has failed: ${message}`, { error });
|
||||
Notifications.error(`Error while reading "${filePath}": ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
+3
-3
@@ -4,16 +4,16 @@
|
||||
*/
|
||||
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 requestHelmChartReadme = di.inject(requestHelmChartReadmeInjectable);
|
||||
|
||||
return asyncComputed(async () => {
|
||||
const chartVersion = selection.value.get();
|
||||
@@ -22,7 +22,7 @@ const readmeOfSelectedHelmChartInjectable = getInjectable({
|
||||
return "";
|
||||
}
|
||||
|
||||
return await callForHelmChartReadme(
|
||||
return await requestHelmChartReadme(
|
||||
chartVersion.getRepository(),
|
||||
chartVersion.getName(),
|
||||
chartVersion.getVersion(),
|
||||
|
||||
-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;
|
||||
+3
-3
@@ -4,18 +4,18 @@
|
||||
*/
|
||||
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);
|
||||
const requestHelmChartVersions = di.inject(requestHelmChartVersionsInjectable);
|
||||
|
||||
return asyncComputed(
|
||||
async () =>
|
||||
await callForHelmChartVersions(chart.getRepository(), chart.getName()),
|
||||
await requestHelmChartVersions(chart.getRepository(), chart.getName()),
|
||||
[],
|
||||
);
|
||||
},
|
||||
|
||||
-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;
|
||||
@@ -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,15 @@
|
||||
*/
|
||||
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(async () => await requestHelmCharts(), []);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+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,29 @@
|
||||
/**
|
||||
* 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(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;
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import callForHelmReleasesInjectable from "./call-for-helm-releases.injectable";
|
||||
import { getGlobalOverride } from "../../../../common/test-utils/get-global-override";
|
||||
|
||||
export default getGlobalOverride(
|
||||
callForHelmReleasesInjectable,
|
||||
() => () => {
|
||||
throw new Error(
|
||||
"Tried to call for helm releases without explicit override.",
|
||||
);
|
||||
},
|
||||
);
|
||||
-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 type { HelmReleaseDto } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
|
||||
import { apiBase } from "../../../../common/k8s-api";
|
||||
import { endpoint } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
|
||||
export type CallForHelmReleases = (
|
||||
namespace?: string
|
||||
) => Promise<HelmReleaseDto[]>;
|
||||
|
||||
const callForHelmReleasesInjectable = getInjectable({
|
||||
id: "call-for-helm-releases",
|
||||
|
||||
instantiate: (): CallForHelmReleases => async (namespace) =>
|
||||
await apiBase.get<HelmReleaseDto[]>(endpoint({ namespace })),
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForHelmReleasesInjectable;
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import yaml from "js-yaml";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { endpoint } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { apiBase } from "../../../../common/k8s-api";
|
||||
|
||||
interface HelmReleaseCreatePayload {
|
||||
name?: string;
|
||||
repo: string;
|
||||
chart: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
values: string;
|
||||
}
|
||||
|
||||
export type CallForCreateHelmRelease = (
|
||||
payload: HelmReleaseCreatePayload
|
||||
) => Promise<HelmReleaseUpdateDetails>;
|
||||
|
||||
const callForCreateHelmReleaseInjectable = getInjectable({
|
||||
id: "call-for-create-helm-release",
|
||||
|
||||
instantiate: (): CallForCreateHelmRelease => (payload) => {
|
||||
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
|
||||
const chart = `${repo}/${rawChart}`;
|
||||
const values = yaml.load(rawValues);
|
||||
|
||||
return apiBase.post(endpoint(), {
|
||||
data: {
|
||||
chart,
|
||||
values,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForCreateHelmReleaseInjectable;
|
||||
@@ -5,15 +5,15 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
|
||||
import releasesInjectable from "../releases.injectable";
|
||||
import type { CallForCreateHelmRelease } from "./call-for-create-helm-release.injectable";
|
||||
import callForCreateHelmReleaseInjectable from "./call-for-create-helm-release.injectable";
|
||||
import type { RequestCreateHelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
|
||||
import requestCreateHelmReleaseInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
|
||||
|
||||
const createReleaseInjectable = getInjectable({
|
||||
id: "create-release",
|
||||
|
||||
instantiate: (di): CallForCreateHelmRelease => {
|
||||
instantiate: (di): RequestCreateHelmRelease => {
|
||||
const releases = di.inject(releasesInjectable);
|
||||
const callForCreateRelease = di.inject(callForCreateHelmReleaseInjectable);
|
||||
const callForCreateRelease = di.inject(requestCreateHelmReleaseInjectable);
|
||||
|
||||
return async (payload) => {
|
||||
const release = await callForCreateRelease(payload);
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type {
|
||||
HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import {
|
||||
deleteRelease,
|
||||
} from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import requestDeleteHelmReleaseInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-delete.injectable";
|
||||
import releasesInjectable from "../releases.injectable";
|
||||
|
||||
const deleteReleaseInjectable = getInjectable({
|
||||
@@ -15,9 +12,10 @@ const deleteReleaseInjectable = getInjectable({
|
||||
|
||||
instantiate: (di) => {
|
||||
const releases = di.inject(releasesInjectable);
|
||||
const requestDeleteHelmRelease = di.inject(requestDeleteHelmReleaseInjectable);
|
||||
|
||||
return async (release: HelmRelease) => {
|
||||
await deleteRelease(release.getName(), release.getNs());
|
||||
await requestDeleteHelmRelease(release.getName(), release.getNs());
|
||||
|
||||
releases.invalidate();
|
||||
};
|
||||
|
||||
@@ -13,20 +13,23 @@ import type { DialogProps } from "../../dialog";
|
||||
import { Dialog } from "../../dialog";
|
||||
import { Wizard, WizardStep } from "../../wizard";
|
||||
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { getReleaseHistory, type HelmReleaseRevision } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { Select } from "../../select";
|
||||
import { Notifications } from "../../notifications";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import releaseRollbackDialogStateInjectable from "./state.injectable";
|
||||
import type { RollbackRelease } from "../rollback-release/rollback-release.injectable";
|
||||
import rollbackReleaseInjectable from "../rollback-release/rollback-release.injectable";
|
||||
import type { HelmReleaseRevision, RequestHelmReleaseHistory } from "../../../../common/k8s-api/endpoints/helm-releases.api/request-history.injectable";
|
||||
import requestHelmReleaseHistoryInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-history.injectable";
|
||||
|
||||
export interface ReleaseRollbackDialogProps extends DialogProps {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
rollbackRelease: (releaseName: string, namespace: string, revisionNumber: number) => Promise<void>;
|
||||
rollbackRelease: RollbackRelease;
|
||||
state: IObservableValue<HelmRelease | undefined>;
|
||||
requestHelmReleaseHistory: RequestHelmReleaseHistory;
|
||||
}
|
||||
|
||||
@observer
|
||||
@@ -48,7 +51,7 @@ class NonInjectedReleaseRollbackDialog extends React.Component<ReleaseRollbackDi
|
||||
onOpen = async (release: HelmRelease) => {
|
||||
this.isLoading.set(true);
|
||||
|
||||
const releases = await getReleaseHistory(release.getName(), release.getNs());
|
||||
const releases = await this.props.requestHelmReleaseHistory(release.getName(), release.getNs());
|
||||
|
||||
runInAction(() => {
|
||||
this.revisions.replace(orderBy(releases, "revision", "desc"));
|
||||
@@ -134,14 +137,11 @@ class NonInjectedReleaseRollbackDialog extends React.Component<ReleaseRollbackDi
|
||||
}
|
||||
}
|
||||
|
||||
export const ReleaseRollbackDialog = withInjectables<Dependencies, ReleaseRollbackDialogProps>(
|
||||
NonInjectedReleaseRollbackDialog,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
rollbackRelease: di.inject(rollbackReleaseInjectable),
|
||||
state: di.inject(releaseRollbackDialogStateInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
export const ReleaseRollbackDialog = withInjectables<Dependencies, ReleaseRollbackDialogProps>(NonInjectedReleaseRollbackDialog, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
rollbackRelease: di.inject(rollbackReleaseInjectable),
|
||||
state: di.inject(releaseRollbackDialogStateInjectable),
|
||||
requestHelmReleaseHistory: di.inject(requestHelmReleaseHistoryInjectable),
|
||||
}),
|
||||
});
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getGlobalOverride } from "../../../../../../common/test-utils/get-global-override";
|
||||
import callForHelmReleaseConfigurationInjectable from "./call-for-helm-release-configuration.injectable";
|
||||
|
||||
export default getGlobalOverride(
|
||||
callForHelmReleaseConfigurationInjectable,
|
||||
() => () => {
|
||||
throw new Error(
|
||||
"Tried to call for helm release configuration without explicit override.",
|
||||
);
|
||||
},
|
||||
);
|
||||
-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 { apiBase } from "../../../../../../common/k8s-api";
|
||||
import { endpoint } from "../../../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
|
||||
export type CallForHelmReleaseConfiguration = (
|
||||
name: string,
|
||||
namespace: string,
|
||||
all: boolean
|
||||
) => Promise<string>;
|
||||
|
||||
const callForHelmReleaseConfigurationInjectable = getInjectable({
|
||||
id: "call-for-helm-release-configuration",
|
||||
|
||||
instantiate:
|
||||
(): CallForHelmReleaseConfiguration => async (name, namespace, all: boolean) => {
|
||||
const route = "values";
|
||||
const path = endpoint({ name, namespace, route }, { all });
|
||||
|
||||
return apiBase.get<string>(path);
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForHelmReleaseConfigurationInjectable;
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getGlobalOverride } from "../../../../../../../common/test-utils/get-global-override";
|
||||
import callForHelmReleaseDetailsInjectable from "./call-for-helm-release-details.injectable";
|
||||
|
||||
export default getGlobalOverride(
|
||||
callForHelmReleaseDetailsInjectable,
|
||||
() => () => {
|
||||
throw new Error(
|
||||
"Tried to call for helm release details without explicit override.",
|
||||
);
|
||||
},
|
||||
);
|
||||
-45
@@ -1,45 +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 { endpoint } from "../../../../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import type { KubeJsonApiData } from "../../../../../../../common/k8s-api/kube-json-api";
|
||||
|
||||
export interface HelmReleaseDetails {
|
||||
resources: KubeJsonApiData[];
|
||||
name: string;
|
||||
namespace: string;
|
||||
version: string;
|
||||
config: string; // release values
|
||||
manifest: string;
|
||||
|
||||
info: {
|
||||
deleted: string;
|
||||
description: string;
|
||||
first_deployed: string;
|
||||
last_deployed: string;
|
||||
notes: string;
|
||||
status: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type CallForHelmReleaseDetails = (
|
||||
name: string,
|
||||
namespace: string
|
||||
) => Promise<HelmReleaseDetails>;
|
||||
|
||||
const callForHelmReleaseDetailsInjectable = getInjectable({
|
||||
id: "call-for-helm-release-details",
|
||||
|
||||
instantiate: (): CallForHelmReleaseDetails => async (name, namespace) => {
|
||||
const path = endpoint({ name, namespace });
|
||||
|
||||
return apiBase.get<HelmReleaseDetails>(path);
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForHelmReleaseDetailsInjectable;
|
||||
-51
@@ -1,51 +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 { HelmReleaseDto } from "../../../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import callForHelmReleasesInjectable from "../../../call-for-helm-releases/call-for-helm-releases.injectable";
|
||||
import type { HelmReleaseDetails } from "./call-for-helm-release-details/call-for-helm-release-details.injectable";
|
||||
import callForHelmReleaseDetailsInjectable from "./call-for-helm-release-details/call-for-helm-release-details.injectable";
|
||||
import type { AsyncResult } from "../../../../../../common/utils/async-result";
|
||||
|
||||
export interface DetailedHelmRelease {
|
||||
release: HelmReleaseDto;
|
||||
details: HelmReleaseDetails;
|
||||
}
|
||||
|
||||
export type CallForHelmRelease = (
|
||||
name: string,
|
||||
namespace: string
|
||||
) => Promise<AsyncResult<DetailedHelmRelease | undefined>>;
|
||||
|
||||
const callForHelmReleaseInjectable = getInjectable({
|
||||
id: "call-for-helm-release",
|
||||
|
||||
instantiate: (di): CallForHelmRelease => {
|
||||
const callForHelmReleases = di.inject(callForHelmReleasesInjectable);
|
||||
const callForHelmReleaseDetails = di.inject(callForHelmReleaseDetailsInjectable);
|
||||
|
||||
return async (name, namespace) => {
|
||||
const [releases, details] = await Promise.all([
|
||||
callForHelmReleases(namespace),
|
||||
callForHelmReleaseDetails(name, namespace),
|
||||
]);
|
||||
|
||||
const release = releases.find(
|
||||
(rel) => rel.name === name && rel.namespace === namespace,
|
||||
);
|
||||
|
||||
if (!release) {
|
||||
return {
|
||||
callWasSuccessful: false,
|
||||
error: `Release ${name} didn't exist in ${namespace} namespace.`,
|
||||
};
|
||||
}
|
||||
|
||||
return { callWasSuccessful: true, response: { release, details }};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default callForHelmReleaseInjectable;
|
||||
+27
-33
@@ -6,18 +6,17 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { IComputedValue, IObservableValue } from "mobx";
|
||||
import { runInAction, action, observable, computed } from "mobx";
|
||||
import type { TargetHelmRelease } from "../target-helm-release.injectable";
|
||||
import type { CallForHelmRelease, DetailedHelmRelease } from "./call-for-helm-release/call-for-helm-release.injectable";
|
||||
import callForHelmReleaseInjectable from "./call-for-helm-release/call-for-helm-release.injectable";
|
||||
import type { RequestDetailedHelmRelease, DetailedHelmRelease } from "./request-detailed-helm-release.injectable";
|
||||
import requestDetailedHelmReleaseInjectable from "./request-detailed-helm-release.injectable";
|
||||
import type { LensTheme } from "../../../../themes/store";
|
||||
import type { CallForHelmReleaseConfiguration } from "./call-for-helm-release-configuration/call-for-helm-release-configuration.injectable";
|
||||
import callForHelmReleaseConfigurationInjectable from "./call-for-helm-release-configuration/call-for-helm-release-configuration.injectable";
|
||||
import { toHelmRelease } from "../../releases.injectable";
|
||||
import type { RequestHelmReleaseConfiguration } from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
|
||||
import requestHelmReleaseConfigurationInjectable from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import { groupBy, map } from "lodash/fp";
|
||||
import type { KubeJsonApiData } from "../../../../../common/k8s-api/kube-json-api";
|
||||
import type { GetResourceDetailsUrl } from "./get-resource-details-url.injectable";
|
||||
import getResourceDetailsUrlInjectable from "./get-resource-details-url.injectable";
|
||||
import type { CallForHelmReleaseUpdate } from "../../update-release/call-for-helm-release-update/call-for-helm-release-update.injectable";
|
||||
import type { RequestHelmReleaseUpdate } from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-update.injectable";
|
||||
import updateReleaseInjectable from "../../update-release/update-release.injectable";
|
||||
import type { ShowCheckedErrorNotification } from "../../../notifications/show-checked-error.injectable";
|
||||
import showCheckedErrorNotificationInjectable from "../../../notifications/show-checked-error.injectable";
|
||||
@@ -26,35 +25,29 @@ import showSuccessNotificationInjectable from "../../../notifications/show-succe
|
||||
import React from "react";
|
||||
import createUpgradeChartTabInjectable from "../../../dock/upgrade-chart/create-upgrade-chart-tab.injectable";
|
||||
import type { HelmRelease } from "../../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import type { NavigateToHelmReleases } from "../../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
|
||||
import navigateToHelmReleasesInjectable from "../../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
|
||||
import assert from "assert";
|
||||
import activeThemeInjectable from "../../../../themes/active.injectable";
|
||||
import type { ToHelmRelease } from "../../to-helm-release.injectable";
|
||||
import toHelmReleaseInjectable from "../../to-helm-release.injectable";
|
||||
|
||||
const releaseDetailsModelInjectable = getInjectable({
|
||||
id: "release-details-model",
|
||||
|
||||
instantiate: async (di, targetRelease: TargetHelmRelease) => {
|
||||
const callForHelmRelease = di.inject(callForHelmReleaseInjectable);
|
||||
const callForHelmReleaseConfiguration = di.inject(callForHelmReleaseConfigurationInjectable);
|
||||
const activeTheme = di.inject(activeThemeInjectable);
|
||||
const getResourceDetailsUrl = di.inject(getResourceDetailsUrlInjectable);
|
||||
const updateRelease = di.inject(updateReleaseInjectable);
|
||||
const showCheckedErrorNotification = di.inject(showCheckedErrorNotificationInjectable);
|
||||
const showSuccessNotification = di.inject(showSuccessNotificationInjectable);
|
||||
const createUpgradeChartTab = di.inject(createUpgradeChartTabInjectable);
|
||||
const navigateToHelmReleases = di.inject(navigateToHelmReleasesInjectable);
|
||||
|
||||
const model = new ReleaseDetailsModel({
|
||||
callForHelmRelease,
|
||||
requestDetailedHelmRelease: di.inject(requestDetailedHelmReleaseInjectable),
|
||||
targetRelease,
|
||||
activeTheme,
|
||||
callForHelmReleaseConfiguration,
|
||||
getResourceDetailsUrl,
|
||||
updateRelease,
|
||||
showCheckedErrorNotification,
|
||||
showSuccessNotification,
|
||||
createUpgradeChartTab,
|
||||
navigateToHelmReleases,
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
requestHelmReleaseConfiguration: di.inject(requestHelmReleaseConfigurationInjectable),
|
||||
getResourceDetailsUrl: di.inject(getResourceDetailsUrlInjectable),
|
||||
updateRelease: di.inject(updateReleaseInjectable),
|
||||
showCheckedErrorNotification: di.inject(showCheckedErrorNotificationInjectable),
|
||||
showSuccessNotification: di.inject(showSuccessNotificationInjectable),
|
||||
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
|
||||
navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable),
|
||||
toHelmRelease: di.inject(toHelmReleaseInjectable),
|
||||
});
|
||||
|
||||
await model.load();
|
||||
@@ -85,20 +78,21 @@ export interface ConfigurationInput {
|
||||
interface Dependencies {
|
||||
readonly targetRelease: TargetHelmRelease;
|
||||
readonly activeTheme: IComputedValue<LensTheme>;
|
||||
callForHelmRelease: CallForHelmRelease;
|
||||
callForHelmReleaseConfiguration: CallForHelmReleaseConfiguration;
|
||||
requestDetailedHelmRelease: RequestDetailedHelmRelease;
|
||||
requestHelmReleaseConfiguration: RequestHelmReleaseConfiguration;
|
||||
getResourceDetailsUrl: GetResourceDetailsUrl;
|
||||
updateRelease: CallForHelmReleaseUpdate;
|
||||
updateRelease: RequestHelmReleaseUpdate;
|
||||
showCheckedErrorNotification: ShowCheckedErrorNotification;
|
||||
showSuccessNotification: ShowNotification;
|
||||
createUpgradeChartTab: (release: HelmRelease) => string;
|
||||
navigateToHelmReleases: () => void;
|
||||
navigateToHelmReleases: NavigateToHelmReleases;
|
||||
toHelmRelease: ToHelmRelease;
|
||||
}
|
||||
|
||||
export class ReleaseDetailsModel {
|
||||
readonly id = `${this.dependencies.targetRelease.namespace}/${this.dependencies.targetRelease.name}`;
|
||||
|
||||
constructor(private readonly dependencies: Dependencies) {}
|
||||
constructor(protected readonly dependencies: Dependencies) {}
|
||||
|
||||
private readonly detailedRelease = observable.box<DetailedHelmRelease | undefined>();
|
||||
|
||||
@@ -171,7 +165,7 @@ export class ReleaseDetailsModel {
|
||||
load = async () => {
|
||||
const { name, namespace } = this.dependencies.targetRelease;
|
||||
|
||||
const result = await this.dependencies.callForHelmRelease(
|
||||
const result = await this.dependencies.requestDetailedHelmRelease(
|
||||
name,
|
||||
namespace,
|
||||
);
|
||||
@@ -199,7 +193,7 @@ export class ReleaseDetailsModel {
|
||||
const { name, namespace } = this.release;
|
||||
|
||||
const configuration =
|
||||
await this.dependencies.callForHelmReleaseConfiguration(
|
||||
await this.dependencies.requestHelmReleaseConfiguration(
|
||||
name,
|
||||
namespace,
|
||||
!this.onlyUserSuppliedValuesAreShown.value.get(),
|
||||
@@ -216,7 +210,7 @@ export class ReleaseDetailsModel {
|
||||
|
||||
assert(detailedRelease, "Tried to access release before load");
|
||||
|
||||
return toHelmRelease(detailedRelease.release);
|
||||
return this.dependencies.toHelmRelease(detailedRelease.release);
|
||||
}
|
||||
|
||||
@computed private get details() {
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 { HelmReleaseDto } from "../../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import requestHelmReleasesInjectable from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
|
||||
import type { HelmReleaseDetails } from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-details.injectable";
|
||||
import requestHelmReleaseDetailsInjectable from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-details.injectable";
|
||||
import type { AsyncResult } from "../../../../../common/utils/async-result";
|
||||
|
||||
export interface DetailedHelmRelease {
|
||||
release: HelmReleaseDto;
|
||||
details: HelmReleaseDetails;
|
||||
}
|
||||
|
||||
export type RequestDetailedHelmRelease = (
|
||||
name: string,
|
||||
namespace: string
|
||||
) => Promise<AsyncResult<DetailedHelmRelease>>;
|
||||
|
||||
const requestDetailedHelmReleaseInjectable = getInjectable({
|
||||
id: "request-detailed-helm-release",
|
||||
|
||||
instantiate: (di): RequestDetailedHelmRelease => {
|
||||
const requestHelmReleases = di.inject(requestHelmReleasesInjectable);
|
||||
const requestHelmReleaseDetails = di.inject(requestHelmReleaseDetailsInjectable);
|
||||
|
||||
return async (name, namespace) => {
|
||||
const [releases, details] = await Promise.all([
|
||||
requestHelmReleases(namespace),
|
||||
requestHelmReleaseDetails(name, namespace),
|
||||
]);
|
||||
|
||||
const release = releases.find(
|
||||
(rel) => rel.name === name && rel.namespace === namespace,
|
||||
);
|
||||
|
||||
if (!release) {
|
||||
return {
|
||||
callWasSuccessful: false,
|
||||
error: `Release ${name} didn't exist in ${namespace} namespace.`,
|
||||
};
|
||||
}
|
||||
|
||||
return { callWasSuccessful: true, response: { release, details }};
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default requestDetailedHelmReleaseInjectable;
|
||||
@@ -61,7 +61,10 @@ class NonInjectedHelmReleaseMenu extends React.Component<HelmReleaseMenuProps &
|
||||
<span className="title">Rollback</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={this.upgrade}>
|
||||
<MenuItem
|
||||
onClick={this.upgrade}
|
||||
data-testid={`upgrade-chart-menu-item-for-${release.getId()}`}
|
||||
>
|
||||
<Icon
|
||||
material="refresh"
|
||||
interactive={toolbar}
|
||||
@@ -79,6 +82,10 @@ class NonInjectedHelmReleaseMenu extends React.Component<HelmReleaseMenuProps &
|
||||
return (
|
||||
<MenuActions
|
||||
id={`menu-actions-for-release-menu-for-${release.getId()}`}
|
||||
triggerIcon={{
|
||||
material: "more_vert",
|
||||
"data-testid": `menu-actions-icon-for-release-menu-for-${release.getId()}`,
|
||||
}}
|
||||
{...menuProps}
|
||||
className={cssNames("HelmReleaseMenu", className)}
|
||||
removeAction={this.remove}
|
||||
|
||||
@@ -4,113 +4,32 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||
import namespaceStoreInjectable from "../+namespaces/store.injectable";
|
||||
import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-frame-context.injectable";
|
||||
import releaseSecretsInjectable from "./release-secrets.injectable";
|
||||
import callForHelmReleasesInjectable from "./call-for-helm-releases/call-for-helm-releases.injectable";
|
||||
import type { HelmRelease, HelmReleaseDto } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { formatDuration } from "../../../common/utils";
|
||||
import { helmChartStore } from "../+helm-charts/helm-chart.store";
|
||||
import { capitalize } from "lodash/fp";
|
||||
import requestHelmReleasesInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
|
||||
import toHelmReleaseInjectable from "./to-helm-release.injectable";
|
||||
|
||||
const releasesInjectable = getInjectable({
|
||||
id: "releases",
|
||||
|
||||
instantiate: (di) => {
|
||||
const clusterContext = di.inject(clusterFrameContextInjectable);
|
||||
const namespaceStore = di.inject(namespaceStoreInjectable);
|
||||
const releaseSecrets = di.inject(releaseSecretsInjectable);
|
||||
const callForHelmReleases = di.inject(callForHelmReleasesInjectable);
|
||||
const requestHelmReleases = di.inject(requestHelmReleasesInjectable);
|
||||
const toHelmRelease = di.inject(toHelmReleaseInjectable);
|
||||
|
||||
return asyncComputed(async () => {
|
||||
const contextNamespaces = namespaceStore.contextNamespaces || [];
|
||||
|
||||
void releaseSecrets.get();
|
||||
|
||||
const isLoadingAll =
|
||||
clusterContext.allNamespaces?.length > 1 &&
|
||||
clusterContext.cluster?.accessibleNamespaces.length === 0 &&
|
||||
clusterContext.allNamespaces.every((namespace) =>
|
||||
contextNamespaces.includes(namespace),
|
||||
);
|
||||
|
||||
const releaseArrays = await (isLoadingAll ? callForHelmReleases() : Promise.all(
|
||||
contextNamespaces.map((namespace) =>
|
||||
callForHelmReleases(namespace),
|
||||
),
|
||||
));
|
||||
const releaseArrays = await (clusterContext.hasSelectedAll
|
||||
? requestHelmReleases()
|
||||
: Promise.all(clusterContext.contextNamespaces.map(namespace => requestHelmReleases(namespace)))
|
||||
);
|
||||
|
||||
return releaseArrays.flat().map(toHelmRelease);
|
||||
}, []);
|
||||
},
|
||||
});
|
||||
|
||||
export const toHelmRelease = (release: HelmReleaseDto) : HelmRelease => ({
|
||||
...release,
|
||||
|
||||
getId() {
|
||||
return `${this.namespace}/${this.name}`;
|
||||
},
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
},
|
||||
|
||||
getNs() {
|
||||
return this.namespace;
|
||||
},
|
||||
|
||||
getChart(withVersion = false) {
|
||||
let chart = this.chart;
|
||||
|
||||
if (!withVersion && this.getVersion() != "") {
|
||||
const search = new RegExp(`-${this.getVersion()}`);
|
||||
|
||||
chart = chart.replace(search, "");
|
||||
}
|
||||
|
||||
return chart;
|
||||
},
|
||||
|
||||
getRevision() {
|
||||
return parseInt(this.revision, 10);
|
||||
},
|
||||
|
||||
getStatus() {
|
||||
return capitalize(this.status);
|
||||
},
|
||||
|
||||
getVersion() {
|
||||
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
|
||||
|
||||
return versions?.[0] ?? "";
|
||||
},
|
||||
|
||||
getUpdated(humanize = true, compact = true) {
|
||||
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
|
||||
const updatedDate = new Date(updated).getTime();
|
||||
const diff = Date.now() - updatedDate;
|
||||
|
||||
if (humanize) {
|
||||
return formatDuration(diff, compact);
|
||||
}
|
||||
|
||||
return diff;
|
||||
},
|
||||
|
||||
// Helm does not store from what repository the release is installed,
|
||||
// so we have to try to guess it by searching charts
|
||||
async getRepo() {
|
||||
const chartName = this.getChart();
|
||||
const version = this.getVersion();
|
||||
const versions = await helmChartStore.getVersions(chartName);
|
||||
const chartVersion = versions.find(
|
||||
(chartVersion) => chartVersion.version === version,
|
||||
);
|
||||
|
||||
return chartVersion ? chartVersion.repo : "";
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export default releasesInjectable;
|
||||
+7
-4
@@ -3,17 +3,20 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { rollbackRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import requestHelmReleaseRollbackInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-rollback.injectable";
|
||||
import releasesInjectable from "../releases.injectable";
|
||||
|
||||
export type RollbackRelease = (name: string, namespace: string, revision: number) => Promise<void>;
|
||||
|
||||
const rollbackReleaseInjectable = getInjectable({
|
||||
id: "rollback-release",
|
||||
|
||||
instantiate: (di) => {
|
||||
instantiate: (di): RollbackRelease => {
|
||||
const releases = di.inject(releasesInjectable);
|
||||
const requestHelmReleaseRollback = di.inject(requestHelmReleaseRollbackInjectable);
|
||||
|
||||
return async (name: string, namespace: string, revision: number) => {
|
||||
await rollbackRelease(name, namespace, revision);
|
||||
return async (name, namespace, revision) => {
|
||||
await requestHelmReleaseRollback(name, namespace, revision);
|
||||
|
||||
releases.invalidate();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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 { capitalize } from "lodash";
|
||||
import { when } from "mobx";
|
||||
import helmChartVersionsInjectable from "../+helm-charts/helm-charts/versions.injectable";
|
||||
import type { HelmRelease, HelmReleaseDto } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { getMillisecondsFromUnixEpoch } from "../../../common/utils/date/get-current-date-time";
|
||||
import { formatDuration } from "../../utils";
|
||||
|
||||
export type ToHelmRelease = (release: HelmReleaseDto) => HelmRelease;
|
||||
|
||||
const toHelmReleaseInjectable = getInjectable({
|
||||
id: "to-helm-release",
|
||||
instantiate: (di): ToHelmRelease => {
|
||||
const helmChartVersions = (release: HelmRelease) => di.inject(helmChartVersionsInjectable, release);
|
||||
|
||||
return (release) => ({
|
||||
...release,
|
||||
|
||||
getId() {
|
||||
return `${this.namespace}/${this.name}`;
|
||||
},
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
},
|
||||
|
||||
getNs() {
|
||||
return this.namespace;
|
||||
},
|
||||
|
||||
getChart(withVersion = false) {
|
||||
let chart = this.chart;
|
||||
|
||||
if (!withVersion && this.getVersion() != "") {
|
||||
const search = new RegExp(`-${this.getVersion()}`);
|
||||
|
||||
chart = chart.replace(search, "");
|
||||
}
|
||||
|
||||
return chart;
|
||||
},
|
||||
|
||||
getRevision() {
|
||||
return parseInt(this.revision, 10);
|
||||
},
|
||||
|
||||
getStatus() {
|
||||
return capitalize(this.status);
|
||||
},
|
||||
|
||||
getVersion() {
|
||||
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
|
||||
|
||||
return versions?.[0] ?? "";
|
||||
},
|
||||
|
||||
getUpdated(humanize = true, compact = true) {
|
||||
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
|
||||
const updatedDate = new Date(updated).getTime();
|
||||
const diff = getMillisecondsFromUnixEpoch() - updatedDate;
|
||||
|
||||
if (humanize) {
|
||||
return formatDuration(diff, compact);
|
||||
}
|
||||
|
||||
return diff;
|
||||
},
|
||||
|
||||
// Helm does not store from what repository the release is installed,
|
||||
// so we have to try to guess it by searching charts
|
||||
async getRepo() {
|
||||
const versionsComputed = helmChartVersions(this);
|
||||
const version = this.getVersion();
|
||||
|
||||
await when(() => !versionsComputed.pending.get());
|
||||
|
||||
return versionsComputed.value
|
||||
.get()
|
||||
.find((chartVersion) => chartVersion.version === version)?.repo
|
||||
?? "";
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default toHelmReleaseInjectable;
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getGlobalOverride } from "../../../../../common/test-utils/get-global-override";
|
||||
import callForHelmReleaseUpdateInjectable from "./call-for-helm-release-update.injectable";
|
||||
|
||||
export default getGlobalOverride(
|
||||
callForHelmReleaseUpdateInjectable,
|
||||
() => () => {
|
||||
throw new Error(
|
||||
"Tried to call for helm release update without explicit override.",
|
||||
);
|
||||
},
|
||||
);
|
||||
-50
@@ -1,50 +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 { endpoint } from "../../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
interface HelmReleaseUpdatePayload {
|
||||
repo: string;
|
||||
chart: string;
|
||||
version: string;
|
||||
values: string;
|
||||
}
|
||||
|
||||
export type CallForHelmReleaseUpdate = (
|
||||
name: string,
|
||||
namespace: string,
|
||||
payload: HelmReleaseUpdatePayload
|
||||
) => Promise<{ updateWasSuccessful: true } | { updateWasSuccessful: false; error: unknown }>;
|
||||
|
||||
const callForHelmReleaseUpdateInjectable = getInjectable({
|
||||
id: "call-for-helm-release-update",
|
||||
|
||||
instantiate:
|
||||
(): CallForHelmReleaseUpdate => async (name, namespace, payload) => {
|
||||
const { repo, chart: rawChart, values: rawValues, ...data } = payload;
|
||||
const chart = `${repo}/${rawChart}`;
|
||||
const values = yaml.load(rawValues);
|
||||
|
||||
try {
|
||||
await apiBase.put(endpoint({ name, namespace }), {
|
||||
data: {
|
||||
chart,
|
||||
values,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return { updateWasSuccessful: false, error: e };
|
||||
}
|
||||
|
||||
return { updateWasSuccessful: true };
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForHelmReleaseUpdateInjectable;
|
||||
@@ -4,15 +4,15 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import releasesInjectable from "../releases.injectable";
|
||||
import type { CallForHelmReleaseUpdate } from "./call-for-helm-release-update/call-for-helm-release-update.injectable";
|
||||
import callForHelmReleaseUpdateInjectable from "./call-for-helm-release-update/call-for-helm-release-update.injectable";
|
||||
import type { RequestHelmReleaseUpdate } from "../../../../common/k8s-api/endpoints/helm-releases.api/request-update.injectable";
|
||||
import requestHelmReleaseUpdateInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-update.injectable";
|
||||
|
||||
const updateReleaseInjectable = getInjectable({
|
||||
id: "update-release",
|
||||
|
||||
instantiate: (di): CallForHelmReleaseUpdate => {
|
||||
instantiate: (di): RequestHelmReleaseUpdate => {
|
||||
const releases = di.inject(releasesInjectable);
|
||||
const callForHelmReleaseUpdate = di.inject(callForHelmReleaseUpdateInjectable);
|
||||
const callForHelmReleaseUpdate = di.inject(requestHelmReleaseUpdateInjectable);
|
||||
|
||||
return async (
|
||||
name,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { computed, makeObservable, observable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { DrawerItem } from "../drawer";
|
||||
import { cssNames } from "../../utils";
|
||||
import { getMetricsForNamespace, type PodMetricData, Namespace } from "../../../common/k8s-api/endpoints";
|
||||
import { Namespace } from "../../../common/k8s-api/endpoints";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Spinner } from "../spinner";
|
||||
@@ -31,6 +31,8 @@ import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-activ
|
||||
import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable";
|
||||
import limitRangeStoreInjectable from "../+config-limit-ranges/store.injectable";
|
||||
import resourceQuotaStoreInjectable from "../+config-resource-quotas/store.injectable";
|
||||
import type { PodMetricInNamespaceData, RequestPodMetricsInNamespace } from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-in-namespace.injectable";
|
||||
import requestPodMetricsInNamespaceInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-in-namespace.injectable";
|
||||
|
||||
export interface NamespaceDetailsProps extends KubeObjectDetailsProps<Namespace> {
|
||||
}
|
||||
@@ -41,11 +43,12 @@ interface Dependencies {
|
||||
getDetailsUrl: GetDetailsUrl;
|
||||
resourceQuotaStore: ResourceQuotaStore;
|
||||
limitRangeStore: LimitRangeStore;
|
||||
requestPodMetricsInNamespace: RequestPodMetricsInNamespace;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedNamespaceDetails extends React.Component<NamespaceDetailsProps & Dependencies> {
|
||||
@observable metrics: PodMetricData | null = null;
|
||||
@observable metrics: PodMetricInNamespaceData | null = null;
|
||||
|
||||
constructor(props: NamespaceDetailsProps & Dependencies) {
|
||||
super(props);
|
||||
@@ -78,7 +81,7 @@ class NonInjectedNamespaceDetails extends React.Component<NamespaceDetailsProps
|
||||
}
|
||||
|
||||
loadMetrics = async () => {
|
||||
this.metrics = await getMetricsForNamespace(this.props.object.getName(), "");
|
||||
this.metrics = await this.props.requestPodMetricsInNamespace(this.props.object.getName());
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -144,6 +147,7 @@ export const NamespaceDetails = withInjectables<Dependencies, NamespaceDetailsPr
|
||||
getDetailsUrl: di.inject(getDetailsUrlInjectable),
|
||||
limitRangeStore: di.inject(limitRangeStoreInjectable),
|
||||
resourceQuotaStore: di.inject(resourceQuotaStoreInjectable),
|
||||
requestPodMetricsInNamespace: di.inject(requestPodMetricsInNamespaceInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -16,19 +16,26 @@ import { ResourceMetrics } from "../resource-metrics";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { IngressCharts } from "./ingress-charts";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { computeRuleDeclarations, getMetricsForIngress, type IngressMetricData } from "../../../common/k8s-api/endpoints/ingress.api";
|
||||
import { computeRuleDeclarations } from "../../../common/k8s-api/endpoints/ingress.api";
|
||||
import { getActiveClusterEntity } from "../../api/catalog/entity/legacy-globals";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import logger from "../../../common/logger";
|
||||
import type { IngressMetricData, RequestIngressMetrics } from "../../../common/k8s-api/endpoints/metrics.api/request-ingress-metrics.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import requestIngressMetricsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-ingress-metrics.injectable";
|
||||
|
||||
export interface IngressDetailsProps extends KubeObjectDetailsProps<Ingress> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
requestIngressMetrics: RequestIngressMetrics;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class IngressDetails extends React.Component<IngressDetailsProps> {
|
||||
class NonInjectedIngressDetails extends React.Component<IngressDetailsProps & Dependencies> {
|
||||
@observable metrics: IngressMetricData | null = null;
|
||||
|
||||
constructor(props: IngressDetailsProps) {
|
||||
constructor(props: IngressDetailsProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -42,9 +49,9 @@ export class IngressDetails extends React.Component<IngressDetailsProps> {
|
||||
}
|
||||
|
||||
loadMetrics = async () => {
|
||||
const { object: ingress } = this.props;
|
||||
const { object: ingress, requestIngressMetrics } = this.props;
|
||||
|
||||
this.metrics = await getMetricsForIngress(ingress.getName(), ingress.getNs());
|
||||
this.metrics = await requestIngressMetrics(ingress.getName(), ingress.getNs());
|
||||
};
|
||||
|
||||
renderPaths(ingress: Ingress) {
|
||||
@@ -170,3 +177,10 @@ export class IngressDetails extends React.Component<IngressDetailsProps> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const IngressDetails = withInjectables<Dependencies, IngressDetailsProps>(NonInjectedIngressDetails, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
requestIngressMetrics: di.inject(requestIngressMetricsInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -13,8 +13,7 @@ import { DrawerItem, DrawerItemLabels } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import { ResourceMetrics } from "../resource-metrics";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { ClusterMetricData } from "../../../common/k8s-api/endpoints";
|
||||
import { formatNodeTaint, getMetricsByNodeNames, Node } from "../../../common/k8s-api/endpoints";
|
||||
import { formatNodeTaint, Node } from "../../../common/k8s-api/endpoints";
|
||||
import { NodeCharts } from "./node-charts";
|
||||
import { makeObservable, observable, reaction } from "mobx";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
@@ -30,6 +29,8 @@ import type { PodStore } from "../+workloads-pods/store";
|
||||
import podStoreInjectable from "../+workloads-pods/store.injectable";
|
||||
import type { GetActiveClusterEntity } from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import type { ClusterMetricData, RequestClusterMetricsByNodeNames } from "../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable";
|
||||
import requestClusterMetricsByNodeNamesInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-cluster-metrics-by-node-names.injectable";
|
||||
|
||||
export interface NodeDetailsProps extends KubeObjectDetailsProps<Node> {
|
||||
}
|
||||
@@ -38,11 +39,12 @@ interface Dependencies {
|
||||
subscribeStores: SubscribeStores;
|
||||
podStore: PodStore;
|
||||
getActiveClusterEntity: GetActiveClusterEntity;
|
||||
requestClusterMetricsByNodeNames: RequestClusterMetricsByNodeNames;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedNodeDetails extends React.Component<NodeDetailsProps & Dependencies> {
|
||||
@observable metrics: Partial<ClusterMetricData> | null = null;
|
||||
@observable metrics: ClusterMetricData | null = null;
|
||||
|
||||
constructor(props: NodeDetailsProps & Dependencies) {
|
||||
super(props);
|
||||
@@ -62,9 +64,9 @@ class NonInjectedNodeDetails extends React.Component<NodeDetailsProps & Dependen
|
||||
}
|
||||
|
||||
loadMetrics = async () => {
|
||||
const { object: node } = this.props;
|
||||
const { object: node, requestClusterMetricsByNodeNames } = this.props;
|
||||
|
||||
this.metrics = await getMetricsByNodeNames([node.getName()]);
|
||||
this.metrics = await requestClusterMetricsByNodeNames([node.getName()]);
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -196,6 +198,7 @@ export const NodeDetails = withInjectables<Dependencies, NodeDetailsProps>(NonIn
|
||||
subscribeStores: di.inject(subscribeStoresInjectable),
|
||||
podStore: di.inject(podStoreInjectable),
|
||||
getActiveClusterEntity: di.inject(getActiveClusterEntityInjectable),
|
||||
requestClusterMetricsByNodeNames: di.inject(requestClusterMetricsByNodeNamesInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -8,10 +8,9 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames, interval } from "../../utils";
|
||||
import { TabLayout } from "../layout/tab-layout-2";
|
||||
import { nodeStore } from "./legacy-store";
|
||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||
import type { NodeMetricData, Node } from "../../../common/k8s-api/endpoints/node.api";
|
||||
import { formatNodeTaint, getMetricsForAllNodes } from "../../../common/k8s-api/endpoints/node.api";
|
||||
import type { Node } from "../../../common/k8s-api/endpoints/node.api";
|
||||
import { formatNodeTaint } from "../../../common/k8s-api/endpoints/node.api";
|
||||
import { LineProgress } from "../line-progress";
|
||||
import { bytesToUnits } from "../../../common/utils/convertMemory";
|
||||
import { Tooltip, TooltipPosition } from "../tooltip";
|
||||
@@ -19,10 +18,15 @@ import kebabCase from "lodash/kebabCase";
|
||||
import upperFirst from "lodash/upperFirst";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { Badge } from "../badge/badge";
|
||||
import { eventStore } from "../+events/legacy-store";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { KubeObjectAge } from "../kube-object/age";
|
||||
import type { NodeMetricData, RequestAllNodeMetrics } from "../../../common/k8s-api/endpoints/metrics.api/request-metrics-for-all-nodes.injectable";
|
||||
import type { NodeStore } from "./store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import nodeStoreInjectable from "./store.injectable";
|
||||
import requestAllNodeMetricsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-metrics-for-all-nodes.injectable";
|
||||
import eventStoreInjectable from "../+events/store.injectable";
|
||||
import type { EventStore } from "../+events/store";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@@ -42,16 +46,23 @@ type MetricsTooltipFormatter = (metrics: [number, number]) => string;
|
||||
interface UsageArgs {
|
||||
node: Node;
|
||||
title: string;
|
||||
metricNames: [string, string];
|
||||
metricNames: [keyof NodeMetricData, keyof NodeMetricData];
|
||||
formatters: MetricsTooltipFormatter[];
|
||||
}
|
||||
|
||||
@observer
|
||||
export class NodesRoute extends React.Component {
|
||||
@observable.ref metrics: Partial<NodeMetricData> = {};
|
||||
private metricsWatcher = interval(30, async () => this.metrics = await getMetricsForAllNodes());
|
||||
interface Dependencies {
|
||||
requestAllNodeMetrics: RequestAllNodeMetrics;
|
||||
nodeStore: NodeStore;
|
||||
eventStore: EventStore;
|
||||
}
|
||||
|
||||
constructor(props: any) {
|
||||
@observer
|
||||
class NonInjectedNodesRoute extends React.Component<Dependencies> {
|
||||
@observable metrics: NodeMetricData | null = null;
|
||||
|
||||
private metricsWatcher = interval(30, async () => this.metrics = await this.props.requestAllNodeMetrics());
|
||||
|
||||
constructor(props: Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -64,8 +75,8 @@ export class NodesRoute extends React.Component {
|
||||
this.metricsWatcher.stop();
|
||||
}
|
||||
|
||||
getLastMetricValues(node: Node, metricNames: string[]): number[] {
|
||||
if (isEmpty(this.metrics)) {
|
||||
getLastMetricValues(node: Node, metricNames: (keyof NodeMetricData)[]): number[] {
|
||||
if (!this.metrics) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -73,7 +84,7 @@ export class NodesRoute extends React.Component {
|
||||
|
||||
return metricNames.map(metricName => {
|
||||
try {
|
||||
const metric = this.metrics[metricName];
|
||||
const metric = this.metrics?.[metricName];
|
||||
const result = metric?.data.result.find(({ metric: { node, instance, kubernetes_node }}) => (
|
||||
nodeName === node
|
||||
|| nodeName === instance
|
||||
@@ -175,6 +186,8 @@ export class NodesRoute extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { nodeStore, eventStore } = this.props;
|
||||
|
||||
return (
|
||||
<TabLayout>
|
||||
<KubeObjectListLayout
|
||||
@@ -251,3 +264,12 @@ export class NodesRoute extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const NodesRoute = withInjectables<Dependencies>(NonInjectedNodesRoute, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
nodeStore: di.inject(nodeStoreInjectable),
|
||||
eventStore: di.inject(eventStoreInjectable),
|
||||
requestAllNodeMetrics: di.inject(requestAllNodeMetricsInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -10,26 +10,39 @@ import { makeObservable, observable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import { podStore } from "../+workloads-pods/legacy-store";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ResourceMetrics } from "../resource-metrics";
|
||||
import { VolumeClaimDiskChart } from "./volume-claim-disk-chart";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { getMetricsForPvc, type PersistentVolumeClaimMetricData, PersistentVolumeClaim } from "../../../common/k8s-api/endpoints";
|
||||
import { getActiveClusterEntity } from "../../api/catalog/entity/legacy-globals";
|
||||
import { PersistentVolumeClaim } from "../../../common/k8s-api/endpoints";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import logger from "../../../common/logger";
|
||||
import type { PersistentVolumeClaimMetricData, RequestPersistentVolumeClaimMetrics } from "../../../common/k8s-api/endpoints/metrics.api/request-persistent-volume-claim-metrics.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import requestPersistentVolumeClaimMetricsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-persistent-volume-claim-metrics.injectable";
|
||||
import type { GetActiveClusterEntity } from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import type { GetDetailsUrl } from "../kube-detail-params/get-details-url.injectable";
|
||||
import type { PodStore } from "../+workloads-pods/store";
|
||||
import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable";
|
||||
import podStoreInjectable from "../+workloads-pods/store.injectable";
|
||||
|
||||
export interface PersistentVolumeClaimDetailsProps extends KubeObjectDetailsProps<PersistentVolumeClaim> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
requestPersistentVolumeClaimMetrics: RequestPersistentVolumeClaimMetrics;
|
||||
getActiveClusterEntity: GetActiveClusterEntity;
|
||||
getDetailsUrl: GetDetailsUrl;
|
||||
podStore: PodStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PersistentVolumeClaimDetails extends React.Component<PersistentVolumeClaimDetailsProps> {
|
||||
class NonInjectedPersistentVolumeClaimDetails extends React.Component<PersistentVolumeClaimDetailsProps & Dependencies> {
|
||||
@observable metrics: PersistentVolumeClaimMetricData | null = null;
|
||||
|
||||
constructor(props: PersistentVolumeClaimDetailsProps) {
|
||||
constructor(props: PersistentVolumeClaimDetailsProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -43,13 +56,13 @@ export class PersistentVolumeClaimDetails extends React.Component<PersistentVolu
|
||||
}
|
||||
|
||||
loadMetrics = async () => {
|
||||
const { object: volumeClaim } = this.props;
|
||||
const { object: volumeClaim, requestPersistentVolumeClaimMetrics } = this.props;
|
||||
|
||||
this.metrics = await getMetricsForPvc(volumeClaim);
|
||||
this.metrics = await requestPersistentVolumeClaimMetrics(volumeClaim);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { object: volumeClaim } = this.props;
|
||||
const { object: volumeClaim, getActiveClusterEntity, podStore, getDetailsUrl } = this.props;
|
||||
|
||||
if (!volumeClaim) {
|
||||
return null;
|
||||
@@ -119,3 +132,13 @@ export class PersistentVolumeClaimDetails extends React.Component<PersistentVolu
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const PersistentVolumeClaimDetails = withInjectables<Dependencies, PersistentVolumeClaimDetailsProps>(NonInjectedPersistentVolumeClaimDetails, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
requestPersistentVolumeClaimMetrics: di.inject(requestPersistentVolumeClaimMetricsInjectable),
|
||||
getActiveClusterEntity: di.inject(getActiveClusterEntityInjectable),
|
||||
getDetailsUrl: di.inject(getDetailsUrlInjectable),
|
||||
podStore: di.inject(podStoreInjectable),
|
||||
}),
|
||||
});
|
||||
+18
-5
@@ -7,20 +7,33 @@ import React from "react";
|
||||
import type { KubeObjectMenuProps } from "../../kube-object-menu";
|
||||
import type { ServiceAccount } from "../../../../common/k8s-api/endpoints";
|
||||
import { MenuItem } from "../../menu";
|
||||
import { openServiceAccountKubeConfig } from "../../kubeconfig-dialog";
|
||||
import { Icon } from "../../icon";
|
||||
import type { OpenServiceAccountKubeConfigDialog } from "../../kubeconfig-dialog/open-service-account-kube-config-dialog.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import openServiceAccountKubeConfigDialogInjectable from "../../kubeconfig-dialog/open-service-account-kube-config-dialog.injectable";
|
||||
|
||||
export function ServiceAccountMenu(props: KubeObjectMenuProps<ServiceAccount>) {
|
||||
const { object, toolbar } = props;
|
||||
interface Dependencies {
|
||||
openServiceAccountKubeConfigDialog: OpenServiceAccountKubeConfigDialog;
|
||||
}
|
||||
|
||||
function NonInjectedServiceAccountMenu(props: KubeObjectMenuProps<ServiceAccount> & Dependencies) {
|
||||
const { object, toolbar, openServiceAccountKubeConfigDialog } = props;
|
||||
|
||||
return (
|
||||
<MenuItem onClick={() => openServiceAccountKubeConfig(object)}>
|
||||
<MenuItem onClick={() => openServiceAccountKubeConfigDialog(object)}>
|
||||
<Icon
|
||||
material="insert_drive_file"
|
||||
tooltip="Kubeconfig File"
|
||||
interactive={toolbar}
|
||||
interactive={toolbar}
|
||||
/>
|
||||
<span className="title">Kubeconfig</span>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export const ServiceAccountMenu = withInjectables<Dependencies, KubeObjectMenuProps<ServiceAccount>>(NonInjectedServiceAccountMenu, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
openServiceAccountKubeConfigDialog: di.inject(openServiceAccountKubeConfigDialogInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -15,7 +15,7 @@ import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities"
|
||||
import type { DaemonSetStore } from "./store";
|
||||
import type { PodStore } from "../+workloads-pods/store";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { DaemonSet, getMetricsForDaemonSets, type PodMetricData } from "../../../common/k8s-api/endpoints";
|
||||
import { DaemonSet } from "../../../common/k8s-api/endpoints";
|
||||
import { ResourceMetrics, ResourceMetricsText } from "../resource-metrics";
|
||||
import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts";
|
||||
import { makeObservable, observable, reaction } from "mobx";
|
||||
@@ -30,6 +30,8 @@ import daemonSetStoreInjectable from "./store.injectable";
|
||||
import podStoreInjectable from "../+workloads-pods/store.injectable";
|
||||
import type { GetActiveClusterEntity } from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import type { DaemonSetPodMetricData, RequestPodMetricsForDaemonSets } from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-daemon-sets.injectable";
|
||||
import requestPodMetricsForDaemonSetsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-daemon-sets.injectable";
|
||||
|
||||
export interface DaemonSetDetailsProps extends KubeObjectDetailsProps<DaemonSet> {
|
||||
}
|
||||
@@ -39,11 +41,12 @@ interface Dependencies {
|
||||
daemonSetStore: DaemonSetStore;
|
||||
podStore: PodStore;
|
||||
getActiveClusterEntity: GetActiveClusterEntity;
|
||||
requestPodMetricsForDaemonSets: RequestPodMetricsForDaemonSets;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedDaemonSetDetails extends React.Component<DaemonSetDetailsProps & Dependencies> {
|
||||
@observable metrics: PodMetricData | null = null;
|
||||
@observable metrics: DaemonSetPodMetricData | null = null;
|
||||
|
||||
constructor(props: DaemonSetDetailsProps & Dependencies) {
|
||||
super(props);
|
||||
@@ -62,9 +65,9 @@ class NonInjectedDaemonSetDetails extends React.Component<DaemonSetDetailsProps
|
||||
}
|
||||
|
||||
loadMetrics = async () => {
|
||||
const { object: daemonSet } = this.props;
|
||||
const { object: daemonSet, requestPodMetricsForDaemonSets } = this.props;
|
||||
|
||||
this.metrics = await getMetricsForDaemonSets([daemonSet], daemonSet.getNs(), "");
|
||||
this.metrics = await requestPodMetricsForDaemonSets([daemonSet], daemonSet.getNs());
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -143,5 +146,6 @@ export const DaemonSetDetails = withInjectables<Dependencies, DaemonSetDetailsPr
|
||||
daemonSetStore: di.inject(daemonSetStoreInjectable),
|
||||
podStore: di.inject(podStoreInjectable),
|
||||
getActiveClusterEntity: di.inject(getActiveClusterEntityInjectable),
|
||||
requestPodMetricsForDaemonSets: di.inject(requestPodMetricsForDaemonSetsInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -10,8 +10,7 @@ import kebabCase from "lodash/kebabCase";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { DrawerItem } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import type { PodMetricData } from "../../../common/k8s-api/endpoints";
|
||||
import { Deployment, getMetricsForDeployments } from "../../../common/k8s-api/endpoints";
|
||||
import { Deployment } from "../../../common/k8s-api/endpoints";
|
||||
import { PodDetailsTolerations } from "../+workloads-pods/pod-details-tolerations";
|
||||
import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
@@ -34,6 +33,8 @@ import replicaSetStoreInjectable from "../+workloads-replicasets/store.injectabl
|
||||
import deploymentStoreInjectable from "./store.injectable";
|
||||
import type { GetActiveClusterEntity } from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import type { DeploymentPodMetricData, RequestPodMetricsForDeployments } from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-deployments.injectable";
|
||||
import requestPodMetricsForDeploymentsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-deployments.injectable";
|
||||
|
||||
export interface DeploymentDetailsProps extends KubeObjectDetailsProps<Deployment> {
|
||||
}
|
||||
@@ -44,11 +45,12 @@ interface Dependencies {
|
||||
replicaSetStore: ReplicaSetStore;
|
||||
deploymentStore: DeploymentStore;
|
||||
getActiveClusterEntity: GetActiveClusterEntity;
|
||||
requestPodMetricsForDeployments: RequestPodMetricsForDeployments;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedDeploymentDetails extends React.Component<DeploymentDetailsProps & Dependencies> {
|
||||
@observable metrics: PodMetricData | null = null;
|
||||
@observable metrics: DeploymentPodMetricData | null = null;
|
||||
|
||||
constructor(props: DeploymentDetailsProps & Dependencies) {
|
||||
super(props);
|
||||
@@ -69,9 +71,9 @@ class NonInjectedDeploymentDetails extends React.Component<DeploymentDetailsProp
|
||||
}
|
||||
|
||||
loadMetrics = async () => {
|
||||
const { object: deployment } = this.props;
|
||||
const { object: deployment, requestPodMetricsForDeployments } = this.props;
|
||||
|
||||
this.metrics = await getMetricsForDeployments([deployment], deployment.getNs(), "");
|
||||
this.metrics = await requestPodMetricsForDeployments([deployment], deployment.getNs());
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -173,6 +175,7 @@ export const DeploymentDetails = withInjectables<Dependencies, DeploymentDetails
|
||||
replicaSetStore: di.inject(replicaSetStoreInjectable),
|
||||
deploymentStore: di.inject(deploymentStoreInjectable),
|
||||
getActiveClusterEntity: di.inject(getActiveClusterEntityInjectable),
|
||||
requestPodMetricsForDeployments: di.inject(requestPodMetricsForDeploymentsInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import { PodDetailsTolerations } from "../+workloads-pods/pod-details-toleration
|
||||
import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities";
|
||||
import type { JobStore } from "./store";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { getMetricsForJobs, type PodMetricData, Job } from "../../../common/k8s-api/endpoints";
|
||||
import { Job } from "../../../common/k8s-api/endpoints";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { makeObservable, observable, reaction } from "mobx";
|
||||
@@ -31,6 +31,8 @@ import podStoreInjectable from "../+workloads-pods/store.injectable";
|
||||
import jobStoreInjectable from "./store.injectable";
|
||||
import type { GetActiveClusterEntity } from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import type { JobPodMetricData, RequestPodMetricsForJobs } from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-jobs.injectable";
|
||||
import requestPodMetricsForJobsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-jobs.injectable";
|
||||
|
||||
export interface JobDetailsProps extends KubeObjectDetailsProps<Job> {
|
||||
}
|
||||
@@ -40,11 +42,12 @@ interface Dependencies {
|
||||
podStore: PodStore;
|
||||
jobStore: JobStore;
|
||||
getActiveClusterEntity: GetActiveClusterEntity;
|
||||
requestPodMetricsForJobs: RequestPodMetricsForJobs;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedJobDetails extends React.Component<JobDetailsProps & Dependencies> {
|
||||
@observable metrics: PodMetricData | null = null;
|
||||
@observable metrics: JobPodMetricData | null = null;
|
||||
|
||||
constructor(props: JobDetailsProps & Dependencies) {
|
||||
super(props);
|
||||
@@ -63,9 +66,9 @@ class NonInjectedJobDetails extends React.Component<JobDetailsProps & Dependenci
|
||||
}
|
||||
|
||||
loadMetrics = async () => {
|
||||
const { object: job } = this.props;
|
||||
const { object: job, requestPodMetricsForJobs } = this.props;
|
||||
|
||||
this.metrics = await getMetricsForJobs([job], job.getNs(), "");
|
||||
this.metrics = await requestPodMetricsForJobs([job], job.getNs(), "");
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -159,6 +162,7 @@ export const JobDetails = withInjectables<Dependencies, JobDetailsProps>(NonInje
|
||||
podStore: di.inject(podStoreInjectable),
|
||||
jobStore: di.inject(jobStoreInjectable),
|
||||
getActiveClusterEntity: di.inject(getActiveClusterEntityInjectable),
|
||||
requestPodMetricsForJobs: di.inject(requestPodMetricsForJobsInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -105,7 +105,7 @@ class NonInjectedPodDetailsContainer extends React.Component<PodDetailsContainer
|
||||
<StatusBrick className={cssNames(state, { ready })}/>
|
||||
{name}
|
||||
</div>
|
||||
{!isMetricHidden && !isInitContainer && (
|
||||
{(!isMetricHidden && !isInitContainer && metrics) && (
|
||||
<ResourceMetrics
|
||||
object={pod}
|
||||
tabs={[
|
||||
|
||||
@@ -10,8 +10,8 @@ import kebabCase from "lodash/kebabCase";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { observable, reaction, makeObservable } from "mobx";
|
||||
import type { PodMetricData } from "../../../common/k8s-api/endpoints";
|
||||
import { nodeApi, Pod, getMetricsForPods } from "../../../common/k8s-api/endpoints";
|
||||
import type { NodeApi } from "../../../common/k8s-api/endpoints";
|
||||
import { Pod } from "../../../common/k8s-api/endpoints";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import { cssNames, toJS } from "../../utils";
|
||||
@@ -24,21 +24,34 @@ import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { getItemMetrics } from "../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { PodCharts, podMetricTabs } from "./pod-charts";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getActiveClusterEntity } from "../../api/catalog/entity/legacy-globals";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import logger from "../../../common/logger";
|
||||
import { PodVolumes } from "./details/volumes/view";
|
||||
import type { PodMetricData, RequestPodMetrics } from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import requestPodMetricsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics.injectable";
|
||||
import type { GetActiveClusterEntity } from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import type { GetDetailsUrl } from "../kube-detail-params/get-details-url.injectable";
|
||||
import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable";
|
||||
import nodeApiInjectable from "../../../common/k8s-api/endpoints/node.api.injectable";
|
||||
|
||||
export interface PodDetailsProps extends KubeObjectDetailsProps<Pod> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
requestPodMetrics: RequestPodMetrics;
|
||||
getActiveClusterEntity: GetActiveClusterEntity;
|
||||
getDetailsUrl: GetDetailsUrl;
|
||||
nodeApi: NodeApi;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PodDetails extends React.Component<PodDetailsProps> {
|
||||
class NonInjectedPodDetails extends React.Component<PodDetailsProps & Dependencies> {
|
||||
@observable metrics: PodMetricData | null = null;
|
||||
@observable containerMetrics: PodMetricData | null = null;
|
||||
|
||||
constructor(props: PodDetailsProps) {
|
||||
constructor(props: PodDetailsProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
@@ -53,14 +66,14 @@ export class PodDetails extends React.Component<PodDetailsProps> {
|
||||
}
|
||||
|
||||
loadMetrics = async () => {
|
||||
const { object: pod } = this.props;
|
||||
const { object: pod, requestPodMetrics } = this.props;
|
||||
|
||||
this.metrics = await getMetricsForPods([pod], pod.getNs());
|
||||
this.containerMetrics = await getMetricsForPods([pod], pod.getNs(), "container, namespace");
|
||||
this.metrics = await requestPodMetrics([pod], pod.getNs());
|
||||
this.containerMetrics = await requestPodMetrics([pod], pod.getNs(), "container, namespace");
|
||||
};
|
||||
|
||||
render() {
|
||||
const { object: pod } = this.props;
|
||||
const { object: pod, getActiveClusterEntity, getDetailsUrl, nodeApi } = this.props;
|
||||
|
||||
if (!pod) {
|
||||
return null;
|
||||
@@ -183,3 +196,13 @@ export class PodDetails extends React.Component<PodDetailsProps> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const PodDetails = withInjectables<Dependencies, PodDetailsProps>(NonInjectedPodDetails, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
requestPodMetrics: di.inject(requestPodMetricsInjectable),
|
||||
getActiveClusterEntity: di.inject(getActiveClusterEntityInjectable),
|
||||
getDetailsUrl: di.inject(getDetailsUrlInjectable),
|
||||
nodeApi: di.inject(nodeApiInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -13,8 +13,7 @@ import { PodDetailsTolerations } from "../+workloads-pods/pod-details-toleration
|
||||
import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { PodMetricData } from "../../../common/k8s-api/endpoints";
|
||||
import { getMetricsForReplicaSets, ReplicaSet } from "../../../common/k8s-api/endpoints";
|
||||
import { ReplicaSet } from "../../../common/k8s-api/endpoints";
|
||||
import { ResourceMetrics, ResourceMetricsText } from "../resource-metrics";
|
||||
import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
@@ -30,6 +29,8 @@ import type { ReplicaSetStore } from "./store";
|
||||
import replicaSetStoreInjectable from "./store.injectable";
|
||||
import type { GetActiveClusterEntity } from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import type { ReplicaSetPodMetricData, RequestPodMetricsForReplicaSets } from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-replica-sets.injectable";
|
||||
import requestPodMetricsForReplicaSetsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-replica-sets.injectable";
|
||||
|
||||
export interface ReplicaSetDetailsProps extends KubeObjectDetailsProps<ReplicaSet> {
|
||||
}
|
||||
@@ -39,11 +40,12 @@ interface Dependencies {
|
||||
podStore: PodStore;
|
||||
replicaSetStore: ReplicaSetStore;
|
||||
getActiveClusterEntity: GetActiveClusterEntity;
|
||||
requestPodMetricsForReplicaSets: RequestPodMetricsForReplicaSets;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedReplicaSetDetails extends React.Component<ReplicaSetDetailsProps & Dependencies> {
|
||||
@observable metrics: PodMetricData | null = null;
|
||||
@observable metrics: ReplicaSetPodMetricData | null = null;
|
||||
|
||||
constructor(props: ReplicaSetDetailsProps & Dependencies) {
|
||||
super(props);
|
||||
@@ -63,9 +65,9 @@ class NonInjectedReplicaSetDetails extends React.Component<ReplicaSetDetailsProp
|
||||
}
|
||||
|
||||
loadMetrics = async () => {
|
||||
const { object: replicaSet } = this.props;
|
||||
const { object: replicaSet, requestPodMetricsForReplicaSets } = this.props;
|
||||
|
||||
this.metrics = await getMetricsForReplicaSets([replicaSet], replicaSet.getNs(), "");
|
||||
this.metrics = await requestPodMetricsForReplicaSets([replicaSet], replicaSet.getNs());
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -144,5 +146,6 @@ export const ReplicaSetDetails = withInjectables<Dependencies, ReplicaSetDetails
|
||||
podStore: di.inject(podStoreInjectable),
|
||||
replicaSetStore: di.inject(replicaSetStoreInjectable),
|
||||
getActiveClusterEntity: di.inject(getActiveClusterEntityInjectable),
|
||||
requestPodMetricsForReplicaSets: di.inject(requestPodMetricsForReplicaSetsInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -15,8 +15,7 @@ import { PodDetailsTolerations } from "../+workloads-pods/pod-details-toleration
|
||||
import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities";
|
||||
import type { StatefulSetStore } from "./store";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { PodMetricData } from "../../../common/k8s-api/endpoints";
|
||||
import { getMetricsForStatefulSets, StatefulSet } from "../../../common/k8s-api/endpoints";
|
||||
import { StatefulSet } from "../../../common/k8s-api/endpoints";
|
||||
import { ResourceMetrics, ResourceMetricsText } from "../resource-metrics";
|
||||
import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
@@ -31,6 +30,8 @@ import podStoreInjectable from "../+workloads-pods/store.injectable";
|
||||
import statefulSetStoreInjectable from "./store.injectable";
|
||||
import type { GetActiveClusterEntity } from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
|
||||
import type { RequestPodMetricsForStatefulSets, StatefulSetPodMetricData } from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-stateful-sets.injectable";
|
||||
import requestPodMetricsForStatefulSetsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-stateful-sets.injectable";
|
||||
|
||||
export interface StatefulSetDetailsProps extends KubeObjectDetailsProps<StatefulSet> {
|
||||
}
|
||||
@@ -40,11 +41,12 @@ interface Dependencies {
|
||||
podStore: PodStore;
|
||||
statefulSetStore: StatefulSetStore;
|
||||
getActiveClusterEntity: GetActiveClusterEntity;
|
||||
requestPodMetricsForStatefulSets: RequestPodMetricsForStatefulSets;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedStatefulSetDetails extends React.Component<StatefulSetDetailsProps & Dependencies> {
|
||||
@observable metrics: PodMetricData | null = null;
|
||||
@observable metrics: StatefulSetPodMetricData | null = null;
|
||||
|
||||
constructor(props: StatefulSetDetailsProps & Dependencies) {
|
||||
super(props);
|
||||
@@ -64,9 +66,9 @@ class NonInjectedStatefulSetDetails extends React.Component<StatefulSetDetailsPr
|
||||
}
|
||||
|
||||
loadMetrics = async () => {
|
||||
const { object: statefulSet } = this.props;
|
||||
const { object: statefulSet, requestPodMetricsForStatefulSets } = this.props;
|
||||
|
||||
this.metrics = await getMetricsForStatefulSets([statefulSet], statefulSet.getNs(), "");
|
||||
this.metrics = await requestPodMetricsForStatefulSets([statefulSet], statefulSet.getNs());
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -143,6 +145,7 @@ export const StatefulSetDetails = withInjectables<Dependencies, StatefulSetDetai
|
||||
podStore: di.inject(podStoreInjectable),
|
||||
statefulSetStore: di.inject(statefulSetStoreInjectable),
|
||||
getActiveClusterEntity: di.inject(getActiveClusterEntityInjectable),
|
||||
requestPodMetricsForStatefulSets: di.inject(requestPodMetricsForStatefulSetsInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
*/
|
||||
|
||||
import "./animate.scss";
|
||||
import React from "react";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { cssNames, noop } from "../../utils";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { RequestAnimationFrame } from "./request-animation-frame.injectable";
|
||||
import requestAnimationFrameInjectable from "./request-animation-frame.injectable";
|
||||
import defaultEnterDurationForAnimatedInjectable from "./default-enter-duration.injectable";
|
||||
import defaultLeaveDurationForAnimatedInjectable from "./default-leave-duration.injectable";
|
||||
|
||||
export type AnimateName = "opacity" | "slide-right" | "opacity-scale" | string;
|
||||
|
||||
@@ -24,115 +25,89 @@ export interface AnimateProps {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
requestAnimationFrame: (callback: () => void) => void;
|
||||
requestAnimationFrame: RequestAnimationFrame;
|
||||
defaultEnterDuration: number;
|
||||
defaultLeaveDuration: number;
|
||||
}
|
||||
|
||||
@observer
|
||||
class DefaultedAnimate extends React.Component<AnimateProps & Dependencies & typeof DefaultedAnimate.defaultProps> {
|
||||
static defaultProps = {
|
||||
name: "opacity",
|
||||
enter: true,
|
||||
onEnter: noop,
|
||||
onLeave: noop,
|
||||
enterDuration: 100,
|
||||
leaveDuration: 100,
|
||||
};
|
||||
const NonInjectedAnimate = (propsAndDeps: AnimateProps & Dependencies) => {
|
||||
const {
|
||||
requestAnimationFrame,
|
||||
defaultEnterDuration,
|
||||
defaultLeaveDuration,
|
||||
...props
|
||||
} = propsAndDeps;
|
||||
const {
|
||||
children,
|
||||
enter = true,
|
||||
enterDuration = defaultEnterDuration,
|
||||
leaveDuration = defaultLeaveDuration,
|
||||
name = "opacity",
|
||||
onEnter: onEnterHandler = noop<[]>,
|
||||
onLeave: onLeaveHandler = noop<[]>,
|
||||
} = props;
|
||||
|
||||
@observable isVisible = !!this.props.enter;
|
||||
@observable statusClassName = {
|
||||
enter: false,
|
||||
leave: false,
|
||||
};
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [showClassNameEnter, setShowClassNameEnter] = useState(false);
|
||||
const [showClassNameLeave, setShowClassNameLeave] = useState(false);
|
||||
|
||||
constructor(props: AnimateProps & Dependencies & typeof DefaultedAnimate.defaultProps) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
const contentElem = React.Children.only(children) as React.ReactElement<React.HTMLAttributes<any>>;
|
||||
const classNames = cssNames("Animate", name, contentElem.props.className, {
|
||||
enter: showClassNameEnter,
|
||||
leave: showClassNameLeave,
|
||||
});
|
||||
|
||||
get contentElem() {
|
||||
return React.Children.only(this.props.children) as React.ReactElement<React.HTMLAttributes<any>>;
|
||||
}
|
||||
|
||||
private toggle(enter: boolean) {
|
||||
useEffect(() => {
|
||||
if (enter) {
|
||||
this.enter();
|
||||
} else {
|
||||
this.leave();
|
||||
}
|
||||
}
|
||||
setIsVisible(true);
|
||||
|
||||
componentDidMount() {
|
||||
this.toggle(this.props.enter);
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
setShowClassNameEnter(true);
|
||||
onEnterHandler();
|
||||
});
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<AnimateProps>): void {
|
||||
const { enter } = this.props;
|
||||
return noop;
|
||||
} else if (isVisible) {
|
||||
setShowClassNameLeave(true);
|
||||
onLeaveHandler();
|
||||
|
||||
if (prevProps.enter !== enter) {
|
||||
this.toggle(enter);
|
||||
}
|
||||
}
|
||||
// Cleanup after duration
|
||||
const handle = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
setShowClassNameEnter(false);
|
||||
setShowClassNameLeave(false);
|
||||
}, leaveDuration);
|
||||
|
||||
enter() {
|
||||
this.isVisible = true; // triggers render() to apply css-animation in existing dom
|
||||
|
||||
this.props.requestAnimationFrame(() => {
|
||||
this.statusClassName.enter = true;
|
||||
this.props.onEnter();
|
||||
});
|
||||
}
|
||||
|
||||
leave() {
|
||||
if (!this.isVisible) return;
|
||||
this.statusClassName.leave = true;
|
||||
this.props.onLeave();
|
||||
this.resetAfterLeaveDuration();
|
||||
}
|
||||
|
||||
resetAfterLeaveDuration() {
|
||||
setTimeout(() => this.reset(), this.props.leaveDuration);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.isVisible = false;
|
||||
this.statusClassName.enter = false;
|
||||
this.statusClassName.leave = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return null;
|
||||
return () => clearTimeout(handle);
|
||||
}
|
||||
|
||||
const { name, enterDuration, leaveDuration } = this.props;
|
||||
const contentElem = this.contentElem;
|
||||
const cssVarsForAnimation = {
|
||||
"--enter-duration": `${enterDuration}ms`,
|
||||
"--leave-duration": `${leaveDuration}ms`,
|
||||
} as React.CSSProperties;
|
||||
return noop;
|
||||
}, [enter]);
|
||||
|
||||
return React.cloneElement(contentElem, {
|
||||
className: cssNames("Animate", name, contentElem.props.className, this.statusClassName),
|
||||
children: contentElem.props.children,
|
||||
style: {
|
||||
...contentElem.props.style,
|
||||
...cssVarsForAnimation,
|
||||
},
|
||||
});
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const NonInjectedAnimate = (props: AnimateProps & Dependencies) => <DefaultedAnimate {...props} />;
|
||||
|
||||
export const Animate = withInjectables<Dependencies, AnimateProps>(
|
||||
NonInjectedAnimate,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
requestAnimationFrame: di.inject(requestAnimationFrameInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
const cssVarsForAnimation = {
|
||||
"--enter-duration": `${enterDuration}ms`,
|
||||
"--leave-duration": `${leaveDuration}ms`,
|
||||
} as React.CSSProperties;
|
||||
|
||||
return React.cloneElement(contentElem, {
|
||||
className: classNames,
|
||||
children: contentElem.props.children,
|
||||
style: {
|
||||
...contentElem.props.style,
|
||||
...cssVarsForAnimation,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const Animate = withInjectables<Dependencies, AnimateProps>(NonInjectedAnimate, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
requestAnimationFrame: di.inject(requestAnimationFrameInjectable),
|
||||
defaultEnterDuration: di.inject(defaultEnterDurationForAnimatedInjectable),
|
||||
defaultLeaveDuration: di.inject(defaultLeaveDurationForAnimatedInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 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 defaultEnterDurationForAnimatedInjectable = getInjectable({
|
||||
id: "default-enter-duration-for-animated",
|
||||
instantiate: () => 100,
|
||||
});
|
||||
|
||||
export default defaultEnterDurationForAnimatedInjectable;
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 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 defaultLeaveDurationForAnimatedInjectable = getInjectable({
|
||||
id: "default-leave-duration-for-animated",
|
||||
instantiate: () => 100,
|
||||
});
|
||||
|
||||
export default defaultLeaveDurationForAnimatedInjectable;
|
||||
@@ -4,9 +4,12 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
|
||||
export type RequestAnimationFrame = (callback: () => void) => void;
|
||||
|
||||
const requestAnimationFrameInjectable = getInjectable({
|
||||
id: "request-animation-frame",
|
||||
instantiate: () => (callback: () => void) => requestAnimationFrame(callback),
|
||||
// NOTE: this cannot be simplified to just `=> requestAnimationFrame` else an Illegal Invocation error will be thrown
|
||||
instantiate: (): RequestAnimationFrame => (callback) => requestAnimationFrame(callback),
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
|
||||
+17
-14
@@ -9,7 +9,6 @@ import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { Input } from "../../input";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import type { ShowNotification } from "../../notifications";
|
||||
import { resolveTilde } from "../../../utils";
|
||||
import { Icon } from "../../icon";
|
||||
import { PathPicker } from "../../path-picker";
|
||||
import { isWindows } from "../../../../common/vars";
|
||||
@@ -17,6 +16,8 @@ import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable";
|
||||
import type { ValidateDirectory } from "../../../../common/fs/validate-directory.injectable";
|
||||
import validateDirectoryInjectable from "../../../../common/fs/validate-directory.injectable";
|
||||
import type { ResolveTilde } from "../../../../common/path/resolve-tilde.injectable";
|
||||
import resolveTildeInjectable from "../../../../common/path/resolve-tilde.injectable";
|
||||
|
||||
export interface ClusterLocalTerminalSettingProps {
|
||||
cluster: Cluster;
|
||||
@@ -24,9 +25,15 @@ export interface ClusterLocalTerminalSettingProps {
|
||||
interface Dependencies {
|
||||
showErrorNotification: ShowNotification;
|
||||
validateDirectory: ValidateDirectory;
|
||||
resolveTilde: ResolveTilde;
|
||||
}
|
||||
|
||||
const NonInjectedClusterLocalTerminalSetting = observer(({ cluster, showErrorNotification, validateDirectory }: Dependencies & ClusterLocalTerminalSettingProps) => {
|
||||
const NonInjectedClusterLocalTerminalSetting = observer(({
|
||||
cluster,
|
||||
showErrorNotification,
|
||||
validateDirectory,
|
||||
resolveTilde,
|
||||
}: Dependencies & ClusterLocalTerminalSettingProps) => {
|
||||
if (!cluster) {
|
||||
return null;
|
||||
}
|
||||
@@ -150,15 +157,11 @@ const NonInjectedClusterLocalTerminalSetting = observer(({ cluster, showErrorNot
|
||||
);
|
||||
});
|
||||
|
||||
export const ClusterLocalTerminalSetting = withInjectables<Dependencies, ClusterLocalTerminalSettingProps>(
|
||||
NonInjectedClusterLocalTerminalSetting,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
showErrorNotification: di.inject(showErrorNotificationInjectable),
|
||||
validateDirectory: di.inject(validateDirectoryInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
export const ClusterLocalTerminalSetting = withInjectables<Dependencies, ClusterLocalTerminalSettingProps>(NonInjectedClusterLocalTerminalSetting, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
showErrorNotification: di.inject(showErrorNotificationInjectable),
|
||||
validateDirectory: di.inject(validateDirectoryInjectable),
|
||||
resolveTilde: di.inject(resolveTildeInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -11,10 +11,10 @@ import type { SelectOption } from "../../select";
|
||||
import { Select } from "../../select";
|
||||
import { Input } from "../../input";
|
||||
import { observable, computed, autorun, makeObservable } from "mobx";
|
||||
import type { MetricProviderInfo } from "../../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { metricsApi } from "../../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { Spinner } from "../../spinner";
|
||||
import type { MetricProviderInfo, RequestMetricsProviders } from "../../../../common/k8s-api/endpoints/metrics.api/request-providers.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import requestMetricsProvidersInjectable from "../../../../common/k8s-api/endpoints/metrics.api/request-providers.injectable";
|
||||
import productNameInjectable from "../../../../common/vars/product-name.injectable";
|
||||
|
||||
export interface ClusterPrometheusSettingProps {
|
||||
@@ -27,6 +27,7 @@ type ProviderValue = typeof autoDetectPrometheus | string;
|
||||
|
||||
interface Dependencies {
|
||||
productName: string;
|
||||
requestMetricsProviders: RequestMetricsProviders;
|
||||
}
|
||||
|
||||
@observer
|
||||
@@ -85,8 +86,7 @@ class NonInjectedClusterPrometheusSetting extends React.Component<ClusterPrometh
|
||||
}),
|
||||
);
|
||||
|
||||
metricsApi
|
||||
.getMetricProviders()
|
||||
this.props.requestMetricsProviders()
|
||||
.then(values => {
|
||||
this.loading = false;
|
||||
this.loadedOptions.replace(values.map(provider => [provider.id, provider]));
|
||||
@@ -174,5 +174,6 @@ export const ClusterPrometheusSetting = withInjectables<Dependencies, ClusterPro
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
productName: di.inject(productNameInjectable),
|
||||
requestMetricsProviders: di.inject(requestMetricsProvidersInjectable),
|
||||
}),
|
||||
});
|
||||
-275
@@ -1,275 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import type { RenderResult } from "@testing-library/react";
|
||||
import * as selectEvent from "react-select-event";
|
||||
import type { CreateCluster } from "../../../../common/cluster/create-cluster-injection-token";
|
||||
import { createClusterInjectionToken } from "../../../../common/cluster/create-cluster-injection-token";
|
||||
import createContextHandlerInjectable from "../../../../main/context-handler/create-context-handler.injectable";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable";
|
||||
import createKubeconfigManagerInjectable from "../../../../main/kubeconfig-manager/create-kubeconfig-manager.injectable";
|
||||
import type { ApplicationBuilder } from "../../test-utils/get-application-builder";
|
||||
import { getApplicationBuilder } from "../../test-utils/get-application-builder";
|
||||
import normalizedPlatformInjectable from "../../../../common/vars/normalized-platform.injectable";
|
||||
import kubectlBinaryNameInjectable from "../../../../main/kubectl/binary-name.injectable";
|
||||
import kubectlDownloadingNormalizedArchInjectable from "../../../../main/kubectl/normalized-arch.injectable";
|
||||
import type { OpenDeleteClusterDialog } from "../open.injectable";
|
||||
import openDeleteClusterDialogInjectable from "../open.injectable";
|
||||
|
||||
const currentClusterServerUrl = "https://localhost";
|
||||
const nonCurrentClusterServerUrl = "http://localhost";
|
||||
const multiClusterConfig = `
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: ${currentClusterServerUrl}
|
||||
name: some-current-context-cluster
|
||||
- cluster:
|
||||
server: ${nonCurrentClusterServerUrl}
|
||||
name: some-non-current-context-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: some-current-context-cluster
|
||||
user: some-user
|
||||
name: some-current-context
|
||||
- context:
|
||||
cluster: some-non-current-context-cluster
|
||||
user: some-user
|
||||
name: some-non-current-context
|
||||
current-context: some-current-context
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: some-user
|
||||
user:
|
||||
token: kubeconfig-user-q4lm4:xxxyyyy
|
||||
`;
|
||||
|
||||
const singleClusterServerUrl = "http://localhost";
|
||||
const singleClusterConfig = `
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: ${singleClusterServerUrl}
|
||||
name: some-cluster
|
||||
contexts:
|
||||
- context:
|
||||
cluster: some-cluster
|
||||
user: some-user
|
||||
name: some-context
|
||||
current-context: some-context
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: some-user
|
||||
user:
|
||||
token: kubeconfig-user-q4lm4:xxxyyyy
|
||||
`;
|
||||
|
||||
describe("<DeleteClusterDialog />", () => {
|
||||
let builder: ApplicationBuilder;
|
||||
|
||||
beforeEach(() => {
|
||||
builder = getApplicationBuilder();
|
||||
|
||||
builder.beforeApplicationStart((mainDi) => {
|
||||
mainDi.override(createContextHandlerInjectable, () => () => undefined as never);
|
||||
mainDi.override(createKubeconfigManagerInjectable, () => () => undefined as never);
|
||||
mainDi.override(kubectlBinaryNameInjectable, () => "kubectl");
|
||||
mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
|
||||
mainDi.override(normalizedPlatformInjectable, () => "darwin");
|
||||
});
|
||||
|
||||
builder.beforeWindowStart((windowDi) => {
|
||||
windowDi.override(storesAndApisCanBeCreatedInjectable, () => true);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows context switcher when deleting current cluster", async () => {
|
||||
const config = new KubeConfig();
|
||||
|
||||
config.loadFromString(multiClusterConfig);
|
||||
|
||||
const rendered = await builder.render();
|
||||
|
||||
const windowDi = builder.applicationWindow.only.di;
|
||||
|
||||
const createCluster = windowDi.inject(createClusterInjectionToken);
|
||||
|
||||
const cluster = createCluster({
|
||||
id: "some-current-context-cluster",
|
||||
contextName: "some-current-context",
|
||||
preferences: {
|
||||
clusterName: "some-current-context-cluster",
|
||||
},
|
||||
kubeConfigPath: "./temp-kube-config",
|
||||
}, {
|
||||
clusterServerUrl: currentClusterServerUrl,
|
||||
});
|
||||
|
||||
const openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable);
|
||||
|
||||
openDeleteClusterDialog({ cluster, config });
|
||||
|
||||
const { getByText } = rendered;
|
||||
|
||||
const menu = getByText("Select new context...");
|
||||
|
||||
expect(menu).toBeInTheDocument();
|
||||
selectEvent.openMenu(menu);
|
||||
|
||||
expect(getByText("some-current-context")).toBeInTheDocument();
|
||||
expect(getByText("some-non-current-context")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("Kubeconfig with different clusters", () => {
|
||||
let rendered: RenderResult;
|
||||
let openDeleteClusterDialog: OpenDeleteClusterDialog;
|
||||
let createCluster: CreateCluster;
|
||||
let config: KubeConfig;
|
||||
|
||||
beforeEach(async () => {
|
||||
config = new KubeConfig();
|
||||
|
||||
config.loadFromString(multiClusterConfig);
|
||||
|
||||
rendered = await builder.render();
|
||||
|
||||
const windowDi = builder.applicationWindow.only.di;
|
||||
|
||||
openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable);
|
||||
createCluster = windowDi.inject(createClusterInjectionToken);
|
||||
});
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
expect(rendered.container).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
it("shows warning when deleting non-current-context cluster", () => {
|
||||
const cluster = createCluster({
|
||||
id: "some-non-current-context-cluster",
|
||||
contextName: "some-non-current-context",
|
||||
preferences: {
|
||||
clusterName: "minikube",
|
||||
},
|
||||
kubeConfigPath: "./temp-kube-config",
|
||||
}, {
|
||||
clusterServerUrl: nonCurrentClusterServerUrl,
|
||||
});
|
||||
|
||||
openDeleteClusterDialog({ cluster, config });
|
||||
|
||||
const message = "The contents of kubeconfig file will be changed!";
|
||||
|
||||
expect(rendered.getByText(message)).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
it("shows warning when deleting current-context cluster", () => {
|
||||
const cluster = createCluster({
|
||||
id: "some-current-context-cluster",
|
||||
contextName: "some-current-context",
|
||||
preferences: {
|
||||
clusterName: "some-current-context-cluster",
|
||||
},
|
||||
kubeConfigPath: "./temp-kube-config",
|
||||
}, {
|
||||
clusterServerUrl: currentClusterServerUrl,
|
||||
});
|
||||
|
||||
openDeleteClusterDialog({ cluster, config });
|
||||
|
||||
expect(rendered.getByTestId("current-context-warning")).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
it("shows context switcher after checkbox click", () => {
|
||||
const cluster = createCluster({
|
||||
id: "some-current-context-cluster",
|
||||
contextName: "some-current-context",
|
||||
preferences: {
|
||||
clusterName: "some-current-context-cluster",
|
||||
},
|
||||
kubeConfigPath: "./temp-kube-config",
|
||||
}, {
|
||||
clusterServerUrl: currentClusterServerUrl,
|
||||
});
|
||||
|
||||
openDeleteClusterDialog({ cluster, config });
|
||||
|
||||
const { getByText, getByTestId } = rendered;
|
||||
const link = getByTestId("context-switch");
|
||||
|
||||
expect(link).toBeInstanceOf(HTMLElement);
|
||||
fireEvent.click(link);
|
||||
|
||||
const menu = getByText("Select new context...");
|
||||
|
||||
expect(menu).toBeInTheDocument();
|
||||
selectEvent.openMenu(menu);
|
||||
|
||||
expect(getByText("some-current-context")).toBeInTheDocument();
|
||||
expect(getByText("some-non-current-context")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("given cluster in internal kubeconfig, when deleting cluster outside of current context, shows warning for internal kubeconfig cluster", () => {
|
||||
const cluster = createCluster({
|
||||
id: "some-non-current-context-cluster",
|
||||
contextName: "some-non-current-context",
|
||||
preferences: {
|
||||
clusterName: "some-non-current-context-cluster",
|
||||
},
|
||||
kubeConfigPath: "./temp-kube-config",
|
||||
}, {
|
||||
clusterServerUrl: nonCurrentClusterServerUrl,
|
||||
});
|
||||
|
||||
const spy = jest.spyOn(cluster, "isInLocalKubeconfig").mockImplementation(() => true);
|
||||
|
||||
openDeleteClusterDialog({ cluster, config });
|
||||
|
||||
expect(rendered.getByTestId("internal-kubeconfig-warning")).toBeInstanceOf(HTMLElement);
|
||||
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Kubeconfig with single cluster", () => {
|
||||
let rendered: RenderResult;
|
||||
let openDeleteClusterDialog: OpenDeleteClusterDialog;
|
||||
let createCluster: CreateCluster;
|
||||
let config: KubeConfig;
|
||||
|
||||
beforeEach(async () => {
|
||||
config = new KubeConfig();
|
||||
|
||||
config.loadFromString(singleClusterConfig);
|
||||
|
||||
rendered = await builder.render();
|
||||
|
||||
const windowDi = builder.applicationWindow.only.di;
|
||||
|
||||
openDeleteClusterDialog = windowDi.inject(openDeleteClusterDialogInjectable);
|
||||
createCluster = windowDi.inject(createClusterInjectionToken);
|
||||
});
|
||||
|
||||
it("shows warning if no other contexts left", () => {
|
||||
const cluster = createCluster({
|
||||
id: "some-cluster",
|
||||
contextName: "some-context",
|
||||
preferences: {
|
||||
clusterName: "some-cluster",
|
||||
},
|
||||
kubeConfigPath: "./temp-kube-config",
|
||||
}, {
|
||||
clusterServerUrl: singleClusterServerUrl,
|
||||
});
|
||||
|
||||
openDeleteClusterDialog({ cluster, config });
|
||||
|
||||
expect(rendered.getByTestId("no-more-contexts-warning")).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
});
|
||||
});
|
||||
-2
@@ -17,8 +17,6 @@ const deleteClusterDialogClusterFrameChildComponentInjectable = getInjectable({
|
||||
}),
|
||||
|
||||
injectionToken: clusterFrameChildComponentInjectionToken,
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default deleteClusterDialogClusterFrameChildComponentInjectable;
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { dumpYaml } from "@kubernetes/client-node";
|
||||
import fs from "fs";
|
||||
import * as lockFile from "proper-lockfile";
|
||||
|
||||
export async function saveKubeconfig(config: KubeConfig, path: string) {
|
||||
try {
|
||||
const release = await lockFile.lock(path);
|
||||
const contents = dumpYaml(JSON.parse(config.exportConfig()));
|
||||
|
||||
await fs.promises.writeFile(path, contents);
|
||||
await release();
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to acquire lock file.\n${e}`);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
|
||||
import saveKubeconfigInjectable from "./save-kubeconfig.injectable";
|
||||
|
||||
export default getGlobalOverride(saveKubeconfigInjectable, () => async () => {
|
||||
throw new Error("tried to save a mondified kubeconfig without override");
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { dumpYaml } from "@kubernetes/client-node";
|
||||
import * as lockFile from "proper-lockfile";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import writeFileInjectable from "../../../common/fs/write-file.injectable";
|
||||
|
||||
export type SaveKubeconfig = (config: KubeConfig, path: string) => Promise<void>;
|
||||
|
||||
const saveKubeconfigInjectable = getInjectable({
|
||||
id: "save-kubeconfig",
|
||||
instantiate: (di): SaveKubeconfig => {
|
||||
const writeFile = di.inject(writeFileInjectable);
|
||||
|
||||
return async (config, filePath) => {
|
||||
const release = await lockFile.lock(filePath);
|
||||
|
||||
try {
|
||||
const contents = dumpYaml(JSON.parse(config.exportConfig()));
|
||||
|
||||
await writeFile(filePath, contents);
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
};
|
||||
},
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default saveKubeconfigInjectable;
|
||||
|
||||
@@ -10,22 +10,34 @@ import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
|
||||
import { Button } from "../button";
|
||||
import { saveKubeconfig } from "./save-config";
|
||||
import { Notifications } from "../notifications";
|
||||
import type { ShowNotification } from "../notifications";
|
||||
import { Dialog } from "../dialog";
|
||||
import { Icon } from "../icon";
|
||||
import { Select } from "../select";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { requestClearClusterAsDeleting, requestDeleteCluster, requestSetClusterAsDeleting } from "../../ipc";
|
||||
import type { HotbarStore } from "../../../common/hotbars/store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
|
||||
import type { DeleteClusterDialogState } from "./state.injectable";
|
||||
import deleteClusterDialogStateInjectable from "./state.injectable";
|
||||
import type { RequestSetClusterAsDeleting } from "../../../features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable";
|
||||
import requestSetClusterAsDeletingInjectable from "../../../features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable";
|
||||
import type { RequestClearClusterAsDeleting } from "../../../features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable";
|
||||
import requestClearClusterAsDeletingInjectable from "../../../features/cluster/delete-dialog/renderer/request-clear-as-deleting.injectable";
|
||||
import type { RequestDeleteCluster } from "../../../features/cluster/delete-dialog/renderer/request-delete.injectable";
|
||||
import requestDeleteClusterInjectable from "../../../features/cluster/delete-dialog/renderer/request-delete.injectable";
|
||||
import type { SaveKubeconfig } from "./save-kubeconfig.injectable";
|
||||
import saveKubeconfigInjectable from "./save-kubeconfig.injectable";
|
||||
import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable";
|
||||
|
||||
export interface Dependencies {
|
||||
interface Dependencies {
|
||||
state: IObservableValue<DeleteClusterDialogState | undefined>;
|
||||
hotbarStore: HotbarStore;
|
||||
requestSetClusterAsDeleting: RequestSetClusterAsDeleting;
|
||||
requestDeleteCluster: RequestDeleteCluster;
|
||||
requestClearClusterAsDeleting: RequestClearClusterAsDeleting;
|
||||
showErrorNotification: ShowNotification;
|
||||
saveKubeconfig: SaveKubeconfig;
|
||||
}
|
||||
|
||||
@observer
|
||||
@@ -61,18 +73,18 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
|
||||
async onDelete(state: DeleteClusterDialogState) {
|
||||
const { cluster, config } = state;
|
||||
|
||||
await requestSetClusterAsDeleting(cluster.id);
|
||||
await this.props.requestSetClusterAsDeleting(cluster.id);
|
||||
this.removeContext(state);
|
||||
this.changeCurrentContext(state);
|
||||
|
||||
try {
|
||||
await saveKubeconfig(config, cluster.kubeConfigPath);
|
||||
await this.props.saveKubeconfig(config, cluster.kubeConfigPath);
|
||||
this.props.hotbarStore.removeAllHotbarItems(cluster.id);
|
||||
await requestDeleteCluster(cluster.id);
|
||||
await this.props.requestDeleteCluster(cluster.id);
|
||||
} catch(error) {
|
||||
Notifications.error(`Cannot remove cluster, failed to process config file. ${error}`);
|
||||
this.props.showErrorNotification(`Cannot remove cluster, failed to process config file. ${error}`);
|
||||
} finally {
|
||||
await requestClearClusterAsDeleting(cluster.id);
|
||||
await this.props.requestClearClusterAsDeleting(cluster.id);
|
||||
}
|
||||
|
||||
this.onClose();
|
||||
@@ -155,6 +167,14 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
|
||||
}
|
||||
|
||||
getWarningMessage({ cluster, config }: DeleteClusterDialogState) {
|
||||
if (cluster.isInLocalKubeconfig()) {
|
||||
return (
|
||||
<p data-testid="internal-kubeconfig-warning">
|
||||
Are you sure you want to delete it? It can be re-added through the copy/paste mechanism.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const contexts = config.contexts.filter(context => context.name !== cluster.contextName);
|
||||
|
||||
if (!contexts.length) {
|
||||
@@ -173,14 +193,6 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
|
||||
);
|
||||
}
|
||||
|
||||
if (cluster.isInLocalKubeconfig()) {
|
||||
return (
|
||||
<p data-testid="internal-kubeconfig-warning">
|
||||
Are you sure you want to delete it? It can be re-added through the copy/paste mechanism.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p data-testid="kubeconfig-change-warning">The contents of kubeconfig file will be changed!</p>
|
||||
);
|
||||
@@ -209,7 +221,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
|
||||
<hr className={styles.hr} />
|
||||
<div className="mt-4">
|
||||
<Checkbox
|
||||
data-testid="context-switch"
|
||||
data-testid="delete-cluster-dialog-context-switch"
|
||||
label={(
|
||||
<>
|
||||
<span className="font-semibold">Select current-context</span>
|
||||
@@ -255,6 +267,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
|
||||
close={this.close}
|
||||
onClose={this.onClose}
|
||||
onOpen={state && (() => this.onOpen(state))}
|
||||
data-testid={state ? "delete-cluster-dialog" : undefined}
|
||||
>
|
||||
{state && this.renderContents(state)}
|
||||
</Dialog>
|
||||
@@ -266,5 +279,10 @@ export const DeleteClusterDialog = withInjectables<Dependencies>(NonInjectedDele
|
||||
getProps: (di) => ({
|
||||
hotbarStore: di.inject(hotbarStoreInjectable),
|
||||
state: di.inject(deleteClusterDialogStateInjectable),
|
||||
requestSetClusterAsDeleting: di.inject(requestSetClusterAsDeletingInjectable),
|
||||
requestClearClusterAsDeleting: di.inject(requestClearClusterAsDeletingInjectable),
|
||||
requestDeleteCluster: di.inject(requestDeleteClusterInjectable),
|
||||
saveKubeconfig: di.inject(saveKubeconfigInjectable),
|
||||
showErrorNotification: di.inject(showErrorNotificationInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -146,13 +146,11 @@ class NonInjectedDialog extends React.PureComponent<DialogProps & Dependencies &
|
||||
};
|
||||
|
||||
render() {
|
||||
const { modal, animated, pinned, "data-testid": testId } = this.props;
|
||||
let { className } = this.props;
|
||||
const { modal, animated, pinned, "data-testid": testId, className } = this.props;
|
||||
|
||||
className = cssNames("Dialog flex center", className, { modal, pinned });
|
||||
let dialog = (
|
||||
<div
|
||||
className={className}
|
||||
className={cssNames("Dialog flex center", className, { modal, pinned })}
|
||||
onClick={stopPropagation}
|
||||
ref={this.ref}
|
||||
data-testid={testId}
|
||||
|
||||
@@ -3,49 +3,35 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import path from "path";
|
||||
import { hasCorrectExtension } from "./has-correct-extension";
|
||||
import readFileInjectable from "../../../../common/fs/read-file.injectable";
|
||||
import readDirInjectable from "../../../../common/fs/read-dir.injectable";
|
||||
import readDirectoryInjectable from "../../../../common/fs/read-directory.injectable";
|
||||
import type { RawTemplates } from "./create-resource-templates.injectable";
|
||||
import type { GetAbsolutePath } from "../../../../common/path/get-absolute-path.injectable";
|
||||
import getAbsolutePathInjectable from "../../../../common/path/get-absolute-path.injectable";
|
||||
import staticFilesDirectoryInjectable from "../../../../common/vars/static-files-directory.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
readDir: (dirPath: string) => Promise<string[]>;
|
||||
readFile: (filePath: string) => Promise<string>;
|
||||
getAbsolutePath: GetAbsolutePath;
|
||||
staticFilesDirectory: string;
|
||||
}
|
||||
|
||||
async function getTemplates({ readDir, readFile, getAbsolutePath, staticFilesDirectory }: Dependencies) {
|
||||
const templatesFolder = getAbsolutePath(staticFilesDirectory, "../templates/create-resource");
|
||||
|
||||
/**
|
||||
* Mapping between file names and their contents
|
||||
*/
|
||||
const templates: [file: string, contents: string][] = [];
|
||||
|
||||
for (const dirEntry of await readDir(templatesFolder)) {
|
||||
if (hasCorrectExtension(dirEntry)) {
|
||||
templates.push([path.parse(dirEntry).name, await readFile(path.join(templatesFolder, dirEntry))]);
|
||||
}
|
||||
}
|
||||
|
||||
return templates;
|
||||
}
|
||||
import joinPathsInjectable from "../../../../common/path/join-paths.injectable";
|
||||
import parsePathInjectable from "../../../../common/path/parse.injectable";
|
||||
|
||||
const lensCreateResourceTemplatesInjectable = getInjectable({
|
||||
id: "lens-create-resource-templates",
|
||||
|
||||
instantiate: async (di): Promise<RawTemplates> => {
|
||||
const templates = await getTemplates({
|
||||
readFile: di.inject(readFileInjectable),
|
||||
readDir: di.inject(readDirInjectable),
|
||||
getAbsolutePath: di.inject(getAbsolutePathInjectable),
|
||||
staticFilesDirectory: di.inject(staticFilesDirectoryInjectable),
|
||||
});
|
||||
const readFile = di.inject(readFileInjectable);
|
||||
const readDir = di.inject(readDirectoryInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable);
|
||||
const parsePath = di.inject(parsePathInjectable);
|
||||
|
||||
/**
|
||||
* Mapping between file names and their contents
|
||||
*/
|
||||
const templates: [file: string, contents: string][] = [];
|
||||
const templatesFolder = joinPaths(staticFilesDirectory, "../templates/create-resource");
|
||||
|
||||
for (const dirEntry of await readDir(templatesFolder)) {
|
||||
if (hasCorrectExtension(dirEntry)) {
|
||||
templates.push([parsePath(dirEntry).name, await readFile(joinPaths(templatesFolder, dirEntry))]);
|
||||
}
|
||||
}
|
||||
|
||||
return ["lens", templates];
|
||||
},
|
||||
|
||||
@@ -3,101 +3,108 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { computed, observable } from "mobx";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { delay, getOrInsert, isErrnoException, waitForPath } from "../../../utils";
|
||||
import { watch } from "chokidar";
|
||||
import { readFile } from "fs/promises";
|
||||
import logger from "../../../../common/logger";
|
||||
import { hasCorrectExtension } from "./has-correct-extension";
|
||||
import type { RawTemplates } from "./create-resource-templates.injectable";
|
||||
|
||||
const userTemplatesFolder = path.join(os.homedir(), ".k8slens", "templates");
|
||||
|
||||
function groupTemplates(templates: Map<string, string>): RawTemplates[] {
|
||||
const res = new Map<string, [string, string][]>();
|
||||
|
||||
for (const [filePath, contents] of templates) {
|
||||
const rawRelative = path.dirname(path.relative(userTemplatesFolder, filePath));
|
||||
const title = rawRelative === "."
|
||||
? "ungrouped"
|
||||
: rawRelative;
|
||||
|
||||
getOrInsert(res, title, []).push([path.parse(filePath).name, contents]);
|
||||
}
|
||||
|
||||
return [...res.entries()];
|
||||
}
|
||||
|
||||
function watchUserCreateResourceTemplates(): IComputedValue<RawTemplates[]> {
|
||||
/**
|
||||
* Map between filePaths and template contents
|
||||
*/
|
||||
const templates = observable.map<string, string>();
|
||||
|
||||
const onAddOrChange = async (filePath: string) => {
|
||||
if (!hasCorrectExtension(filePath)) {
|
||||
// ignore non yaml or json files
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const contents = await readFile(filePath, "utf-8");
|
||||
|
||||
templates.set(filePath, contents);
|
||||
} catch (error) {
|
||||
if (isErrnoException(error) && error.code === "ENOENT") {
|
||||
// ignore, file disappeared
|
||||
} else {
|
||||
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while reading ${filePath}`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
const onUnlink = (filePath: string) => {
|
||||
templates.delete(filePath);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
for (let i = 1;; i *= 2) {
|
||||
try {
|
||||
await waitForPath(userTemplatesFolder);
|
||||
break;
|
||||
} catch (error) {
|
||||
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while waiting for ${userTemplatesFolder} to exist, waiting and trying again`, error);
|
||||
await delay(i * 1000); // exponential backoff in seconds
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: There is technically a race condition here of the form "time-of-check to time-of-use"
|
||||
*/
|
||||
watch(userTemplatesFolder, {
|
||||
disableGlobbing: true,
|
||||
ignorePermissionErrors: true,
|
||||
usePolling: false,
|
||||
awaitWriteFinish: {
|
||||
pollInterval: 100,
|
||||
stabilityThreshold: 1000,
|
||||
},
|
||||
ignoreInitial: false,
|
||||
atomic: 150, // for "atomic writes"
|
||||
})
|
||||
.on("add", onAddOrChange)
|
||||
.on("change", onAddOrChange)
|
||||
.on("unlink", onUnlink)
|
||||
.on("error", error => {
|
||||
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while watching files under ${userTemplatesFolder}`, error);
|
||||
});
|
||||
})();
|
||||
|
||||
return computed(() => groupTemplates(templates));
|
||||
}
|
||||
import joinPathsInjectable from "../../../../common/path/join-paths.injectable";
|
||||
import watchInjectable from "../../../../common/fs/watch/watch.injectable";
|
||||
import getRelativePathInjectable from "../../../../common/path/get-relative-path.injectable";
|
||||
import homeDirectoryPathInjectable from "../../../../common/os/home-directory-path.injectable";
|
||||
import getDirnameOfPathInjectable from "../../../../common/path/get-dirname.injectable";
|
||||
import loggerInjectable from "../../../../common/logger.injectable";
|
||||
import parsePathInjectable from "../../../../common/path/parse.injectable";
|
||||
|
||||
const userCreateResourceTemplatesInjectable = getInjectable({
|
||||
id: "user-create-resource-templates",
|
||||
instantiate: () => watchUserCreateResourceTemplates(),
|
||||
instantiate: (di) => {
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const watch = di.inject(watchInjectable);
|
||||
const getRelativePath = di.inject(getRelativePathInjectable);
|
||||
const homeDirectoryPath = di.inject(homeDirectoryPathInjectable);
|
||||
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
const parsePath = di.inject(parsePathInjectable);
|
||||
|
||||
const userTemplatesFolder = joinPaths(homeDirectoryPath, ".k8slens", "templates");
|
||||
const groupTemplates = (templates: Map<string, string>): RawTemplates[] => {
|
||||
const res = new Map<string, [string, string][]>();
|
||||
|
||||
for (const [filePath, contents] of templates) {
|
||||
const rawRelative = getDirnameOfPath(getRelativePath(userTemplatesFolder, filePath));
|
||||
const title = rawRelative === "."
|
||||
? "ungrouped"
|
||||
: rawRelative;
|
||||
|
||||
getOrInsert(res, title, []).push([parsePath(filePath).name, contents]);
|
||||
}
|
||||
|
||||
return [...res.entries()];
|
||||
};
|
||||
|
||||
/**
|
||||
* Map between filePaths and template contents
|
||||
*/
|
||||
const templates = observable.map<string, string>();
|
||||
|
||||
const onAddOrChange = async (filePath: string) => {
|
||||
if (!hasCorrectExtension(filePath)) {
|
||||
// ignore non yaml or json files
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const contents = await readFile(filePath, "utf-8");
|
||||
|
||||
templates.set(filePath, contents);
|
||||
} catch (error) {
|
||||
if (isErrnoException(error) && error.code === "ENOENT") {
|
||||
// ignore, file disappeared
|
||||
} else {
|
||||
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while reading ${filePath}`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
const onUnlink = (filePath: string) => {
|
||||
templates.delete(filePath);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
for (let i = 1;; i *= 2) {
|
||||
try {
|
||||
await waitForPath(userTemplatesFolder);
|
||||
break;
|
||||
} catch (error) {
|
||||
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while waiting for ${userTemplatesFolder} to exist, waiting and trying again`, error);
|
||||
await delay(i * 1000); // exponential backoff in seconds
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: There is technically a race condition here of the form "time-of-check to time-of-use"
|
||||
*/
|
||||
watch(userTemplatesFolder, {
|
||||
disableGlobbing: true,
|
||||
ignorePermissionErrors: true,
|
||||
usePolling: false,
|
||||
awaitWriteFinish: {
|
||||
pollInterval: 100,
|
||||
stabilityThreshold: 1000,
|
||||
},
|
||||
ignoreInitial: false,
|
||||
atomic: 150, // for "atomic writes"
|
||||
})
|
||||
.on("add", onAddOrChange)
|
||||
.on("change", onAddOrChange)
|
||||
.on("unlink", onUnlink)
|
||||
.on("error", error => {
|
||||
logger.warn(`[USER-CREATE-RESOURCE-TEMPLATES]: encountered error while watching files under ${userTemplatesFolder}`, error);
|
||||
});
|
||||
})();
|
||||
|
||||
return computed(() => groupTemplates(templates));
|
||||
},
|
||||
});
|
||||
|
||||
export default userCreateResourceTemplatesInjectable;
|
||||
@@ -6,14 +6,13 @@
|
||||
import React from "react";
|
||||
import type { SelectOption } from "../../select";
|
||||
import { Select } from "../../select";
|
||||
import yaml from "js-yaml";
|
||||
import yaml, { dump } from "js-yaml";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import type { CreateResourceTabStore } from "./store";
|
||||
import { EditorPanel } from "../editor-panel";
|
||||
import { InfoPanel } from "../info-panel";
|
||||
import * as resourceApplierApi from "../../../../common/k8s-api/endpoints/resource-applier.api";
|
||||
import { Notifications } from "../../notifications";
|
||||
import logger from "../../../../common/logger";
|
||||
import type { ApiManager } from "../../../../common/k8s-api/api-manager";
|
||||
@@ -28,6 +27,8 @@ import type { GetDetailsUrl } from "../../kube-detail-params/get-details-url.inj
|
||||
import apiManagerInjectable from "../../../../common/k8s-api/api-manager/manager.injectable";
|
||||
import getDetailsUrlInjectable from "../../kube-detail-params/get-details-url.injectable";
|
||||
import navigateInjectable from "../../../navigation/navigate.injectable";
|
||||
import type { RequestKubeObjectCreation } from "../../../../common/k8s-api/endpoints/resource-applier.api/request-update.injectable";
|
||||
import requestKubeObjectCreationInjectable from "../../../../common/k8s-api/endpoints/resource-applier.api/request-update.injectable";
|
||||
|
||||
export interface CreateResourceProps {
|
||||
tabId: string;
|
||||
@@ -39,6 +40,7 @@ interface Dependencies {
|
||||
apiManager: ApiManager;
|
||||
navigate: Navigate;
|
||||
getDetailsUrl: GetDetailsUrl;
|
||||
requestKubeObjectCreation: RequestKubeObjectCreation;
|
||||
}
|
||||
|
||||
@observer
|
||||
@@ -68,7 +70,7 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
|
||||
};
|
||||
|
||||
create = async (): Promise<void> => {
|
||||
const { apiManager, getDetailsUrl, navigate } = this.props;
|
||||
const { apiManager, getDetailsUrl, navigate, requestKubeObjectCreation } = this.props;
|
||||
|
||||
if (this.error || !this.data?.trim()) {
|
||||
// do not save when field is empty or there is an error
|
||||
@@ -84,7 +86,7 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
|
||||
|
||||
const creatingResources = resources.map(async (resource) => {
|
||||
try {
|
||||
const data = await resourceApplierApi.update(resource);
|
||||
const data = await requestKubeObjectCreation(dump(resource));
|
||||
const { kind, apiVersion, metadata: { name, namespace }} = data;
|
||||
|
||||
const showDetails = () => {
|
||||
@@ -168,5 +170,6 @@ export const CreateResource = withInjectables<Dependencies, CreateResourceProps>
|
||||
apiManager: di.inject(apiManagerInjectable),
|
||||
getDetailsUrl: di.inject(getDetailsUrlInjectable),
|
||||
navigate: di.inject(navigateInjectable),
|
||||
requestKubeObjectCreation: di.inject(requestKubeObjectCreationInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -103,6 +103,7 @@ class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
|
||||
material="close"
|
||||
tooltip={`Close ${this.props.isMac ? "⌘+W" : "Ctrl+W"}`}
|
||||
onClick={close}
|
||||
data-testid={`dock-tab-close-for-${id}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
-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 { getChartValues } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
|
||||
export type CallForHelmChartValues = (
|
||||
repo: string,
|
||||
name: string,
|
||||
version: string
|
||||
) => Promise<string>;
|
||||
|
||||
const callForHelmChartValuesInjectable = getInjectable({
|
||||
id: "call-for-helm-chart-values",
|
||||
instantiate: (): CallForHelmChartValues => getChartValues,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForHelmChartValuesInjectable;
|
||||
@@ -5,36 +5,31 @@
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import installChartTabStoreInjectable from "./store.injectable";
|
||||
import { waitUntilDefined } from "../../../../common/utils";
|
||||
import type { CallForHelmChartValues } from "./chart-data/call-for-helm-chart-values.injectable";
|
||||
import callForHelmChartValuesInjectable from "./chart-data/call-for-helm-chart-values.injectable";
|
||||
import type { IChartInstallData, InstallChartTabStore } from "./store";
|
||||
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import React from "react";
|
||||
import {
|
||||
action,
|
||||
computed,
|
||||
observable,
|
||||
runInAction,
|
||||
} from "mobx";
|
||||
import { action, computed, observable, runInAction } from "mobx";
|
||||
import assert from "assert";
|
||||
import type { CallForCreateHelmRelease } from "../../+helm-releases/create-release/call-for-create-helm-release.injectable";
|
||||
import callForCreateHelmReleaseInjectable from "../../+helm-releases/create-release/call-for-create-helm-release.injectable";
|
||||
import type { RequestCreateHelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
|
||||
import requestCreateHelmReleaseInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
|
||||
import type { HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import dockStoreInjectable from "../dock/store.injectable";
|
||||
import type { NavigateToHelmReleases } from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
|
||||
import navigateToHelmReleasesInjectable from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
|
||||
import type { SingleValue } from "react-select";
|
||||
import type { CallForHelmChartVersions } from "../../+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
|
||||
import callForHelmChartVersionsInjectable from "../../+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
|
||||
import type { RequestHelmChartValues } from "../../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable";
|
||||
import requestHelmChartValuesInjectable from "../../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable";
|
||||
import type { RequestHelmChartVersions } from "../../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
|
||||
import requestHelmChartVersionsInjectable from "../../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
|
||||
|
||||
const installChartModelInjectable = getInjectable({
|
||||
id: "install-chart-model",
|
||||
|
||||
instantiate: async (di, tabId: string) => {
|
||||
const store = di.inject(installChartTabStoreInjectable);
|
||||
const callForHelmChartValues = di.inject(callForHelmChartValuesInjectable);
|
||||
const callForHelmChartVersions = di.inject(callForHelmChartVersionsInjectable);
|
||||
const callForCreateHelmRelease = di.inject(callForCreateHelmReleaseInjectable);
|
||||
const requestHelmChartValues = di.inject(requestHelmChartValuesInjectable);
|
||||
const requestHelmChartVersions = di.inject(requestHelmChartVersionsInjectable);
|
||||
const callForCreateHelmRelease = di.inject(requestCreateHelmReleaseInjectable);
|
||||
const dockStore = di.inject(dockStoreInjectable);
|
||||
const navigateToHelmReleases = di.inject(navigateToHelmReleasesInjectable);
|
||||
const closeTab = () => dockStore.closeTab(tabId);
|
||||
@@ -49,8 +44,8 @@ const installChartModelInjectable = getInjectable({
|
||||
callForCreateHelmRelease,
|
||||
closeTab,
|
||||
navigateToHelmReleases,
|
||||
callForHelmChartValues,
|
||||
callForHelmChartVersions,
|
||||
requestHelmChartValues,
|
||||
requestHelmChartVersions,
|
||||
store,
|
||||
});
|
||||
|
||||
@@ -71,9 +66,9 @@ interface Dependencies {
|
||||
closeTab: () => void;
|
||||
navigateToHelmReleases: NavigateToHelmReleases;
|
||||
waitForChart: () => Promise<void>;
|
||||
callForCreateHelmRelease: CallForCreateHelmRelease;
|
||||
callForHelmChartValues: CallForHelmChartValues;
|
||||
callForHelmChartVersions: CallForHelmChartVersions;
|
||||
callForCreateHelmRelease: RequestCreateHelmRelease;
|
||||
requestHelmChartValues: RequestHelmChartValues;
|
||||
requestHelmChartVersions: RequestHelmChartVersions;
|
||||
store: InstallChartTabStore;
|
||||
}
|
||||
|
||||
@@ -127,7 +122,7 @@ export class InstallChartModel {
|
||||
this.configuration.isLoading.set(true);
|
||||
});
|
||||
|
||||
const configuration = await this.dependencies.callForHelmChartValues(
|
||||
const configuration = await this.dependencies.requestHelmChartValues(
|
||||
this.chart.repo,
|
||||
this.chart.name,
|
||||
version,
|
||||
@@ -193,13 +188,13 @@ export class InstallChartModel {
|
||||
await this.dependencies.waitForChart();
|
||||
|
||||
const [defaultConfiguration, versions] = await Promise.all([
|
||||
this.dependencies.callForHelmChartValues(
|
||||
this.dependencies.requestHelmChartValues(
|
||||
this.chart.repo,
|
||||
this.chart.name,
|
||||
this.chart.version,
|
||||
),
|
||||
|
||||
this.dependencies.callForHelmChartVersions(
|
||||
this.dependencies.requestHelmChartVersions(
|
||||
this.chart.repo,
|
||||
this.chart.name,
|
||||
),
|
||||
|
||||
@@ -3,23 +3,27 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { IChartUpgradeData } from "./store.injectable";
|
||||
import upgradeChartTabStoreInjectable from "./store.injectable";
|
||||
import dockStoreInjectable from "../dock/store.injectable";
|
||||
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import type { DockStore, DockTabCreateSpecific, TabId } from "../dock/store";
|
||||
import { TabKind } from "../dock/store";
|
||||
import type { UpgradeChartTabStore } from "./store";
|
||||
import { runInAction } from "mobx";
|
||||
import getRandomUpgradeChartTabIdInjectable from "./get-random-upgrade-chart-tab-id.injectable";
|
||||
import type { DockTabStore } from "../dock-tab-store/dock-tab.store";
|
||||
|
||||
interface Dependencies {
|
||||
upgradeChartStore: UpgradeChartTabStore;
|
||||
upgradeChartStore: DockTabStore<IChartUpgradeData>;
|
||||
dockStore: DockStore;
|
||||
getRandomId: () => string;
|
||||
}
|
||||
|
||||
const createUpgradeChartTab = ({ upgradeChartStore, dockStore, getRandomId }: Dependencies) => (release: HelmRelease, tabParams: DockTabCreateSpecific = {}): TabId => {
|
||||
const tabId = upgradeChartStore.getTabIdByRelease(release.getName());
|
||||
const tabId = upgradeChartStore.findTabIdFromData(val => (
|
||||
val.releaseName === release.getName()
|
||||
&& val.releaseNamespace === release.getNs()
|
||||
));
|
||||
|
||||
if (tabId) {
|
||||
dockStore.open();
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { UpgradeChartTabStore } from "./store";
|
||||
import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable";
|
||||
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||
|
||||
export interface IChartUpgradeData {
|
||||
releaseName: string;
|
||||
releaseNamespace: string;
|
||||
}
|
||||
|
||||
const upgradeChartTabStoreInjectable = getInjectable({
|
||||
id: "upgrade-chart-tab-store",
|
||||
@@ -13,9 +16,8 @@ const upgradeChartTabStoreInjectable = getInjectable({
|
||||
instantiate: (di) => {
|
||||
const createDockTabStore = di.inject(createDockTabStoreInjectable);
|
||||
|
||||
return new UpgradeChartTabStore({
|
||||
createStorage: di.inject(createStorageInjectable),
|
||||
valuesStore: createDockTabStore<string>(),
|
||||
return createDockTabStore<IChartUpgradeData>({
|
||||
storageKey: "chart_releases",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { action, computed, makeObservable } from "mobx";
|
||||
import type { TabId } from "../dock/store";
|
||||
import type { DockTabStoreDependencies } from "../dock-tab-store/dock-tab.store";
|
||||
import { DockTabStore } from "../dock-tab-store/dock-tab.store";
|
||||
import { getReleaseValues } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import assert from "assert";
|
||||
|
||||
export interface IChartUpgradeData {
|
||||
releaseName: string;
|
||||
releaseNamespace: string;
|
||||
}
|
||||
|
||||
export interface UpgradeChartTabStoreDependencies extends DockTabStoreDependencies {
|
||||
valuesStore: DockTabStore<string>;
|
||||
}
|
||||
|
||||
export class UpgradeChartTabStore extends DockTabStore<IChartUpgradeData> {
|
||||
@computed private get releaseNameReverseLookup(): Map<string, string> {
|
||||
return new Map(this.getAllData().map(([id, { releaseName }]) => [releaseName, id]));
|
||||
}
|
||||
|
||||
get values() {
|
||||
return this.dependencies.valuesStore;
|
||||
}
|
||||
|
||||
constructor(protected readonly dependencies: UpgradeChartTabStoreDependencies) {
|
||||
super(dependencies, {
|
||||
storageKey: "chart_releases",
|
||||
});
|
||||
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@action
|
||||
async reloadValues(tabId: TabId) {
|
||||
this.values.clearData(tabId); // reset
|
||||
const data = this.getData(tabId);
|
||||
|
||||
assert(data, "cannot reload values if no data");
|
||||
|
||||
const { releaseName, releaseNamespace } = data;
|
||||
const values = await getReleaseValues(releaseName, releaseNamespace, true);
|
||||
|
||||
this.values.setData(tabId, values);
|
||||
}
|
||||
|
||||
getTabIdByRelease(releaseName: string) {
|
||||
return this.releaseNameReverseLookup.get(releaseName);
|
||||
}
|
||||
}
|
||||
@@ -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, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { waitUntilDefined } from "../../../utils";
|
||||
import upgradeChartTabStoreInjectable from "./store.injectable";
|
||||
|
||||
const upgradeChartTabDataInjectable = getInjectable({
|
||||
id: "upgrade-chart-tab-data",
|
||||
instantiate: (di, tabId) => {
|
||||
const upgradeChartTabStore = di.inject(upgradeChartTabStoreInjectable);
|
||||
|
||||
return waitUntilDefined(() => upgradeChartTabStore.getData(tabId));
|
||||
},
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, tabId: string) => tabId,
|
||||
}),
|
||||
});
|
||||
|
||||
export default upgradeChartTabDataInjectable;
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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 type { IComputedValue } from "mobx";
|
||||
import { action, computed, observable, when } from "mobx";
|
||||
import type { SingleValue } from "react-select";
|
||||
import type { HelmChartVersion } from "../../+helm-charts/helm-charts/versions";
|
||||
import helmChartVersionsInjectable from "../../+helm-charts/helm-charts/versions.injectable";
|
||||
import releasesInjectable from "../../+helm-releases/releases.injectable";
|
||||
import updateReleaseInjectable from "../../+helm-releases/update-release/update-release.injectable";
|
||||
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import requestHelmReleaseConfigurationInjectable from "../../../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
|
||||
import { waitUntilDefined } from "../../../utils";
|
||||
import type { SelectOption } from "../../select";
|
||||
import type { DockTab } from "../dock/store";
|
||||
import upgradeChartTabDataInjectable from "./tab-data.injectable";
|
||||
|
||||
export interface UpgradeChartModel {
|
||||
readonly release: HelmRelease;
|
||||
readonly versionOptions: IComputedValue<SelectOption<HelmChartVersion>[]>;
|
||||
readonly configration: {
|
||||
readonly value: IComputedValue<string>;
|
||||
set: (value: string) => void;
|
||||
readonly error: IComputedValue<string | undefined>;
|
||||
setError: (error: unknown) => void;
|
||||
};
|
||||
readonly version: {
|
||||
readonly value: IComputedValue<HelmChartVersion | undefined>;
|
||||
set: (value: SingleValue<SelectOption<HelmChartVersion>>) => void;
|
||||
};
|
||||
submit: () => Promise<UpgradeChartSubmitResult>;
|
||||
}
|
||||
|
||||
export interface UpgradeChartSubmitResult {
|
||||
completedSuccessfully: boolean;
|
||||
}
|
||||
|
||||
const upgradeChartModelInjectable = getInjectable({
|
||||
id: "upgrade-chart-model",
|
||||
instantiate: async (di, tab): Promise<UpgradeChartModel> => {
|
||||
const releases = di.inject(releasesInjectable);
|
||||
const requestHelmReleaseConfiguration = di.inject(requestHelmReleaseConfigurationInjectable);
|
||||
const updateRelease = di.inject(updateReleaseInjectable);
|
||||
const tabData = await di.inject(upgradeChartTabDataInjectable, tab.id);
|
||||
|
||||
const release = await waitUntilDefined(() => (
|
||||
releases.value
|
||||
.get()
|
||||
.find(release => (
|
||||
release.getName() === tabData.releaseName
|
||||
&& release.getNs() === tabData.releaseNamespace
|
||||
))
|
||||
));
|
||||
const versions = di.inject(helmChartVersionsInjectable, release);
|
||||
const storedConfiguration = asyncComputed(() => requestHelmReleaseConfiguration(
|
||||
release.getName(),
|
||||
release.getNs(),
|
||||
true,
|
||||
), "");
|
||||
|
||||
await when(() => !versions.pending.get());
|
||||
|
||||
const configrationValue = observable.box<string>();
|
||||
const configrationEditError = observable.box<string>();
|
||||
const configration: UpgradeChartModel["configration"] = {
|
||||
value: computed(() => configrationValue.get() ?? storedConfiguration.value.get()),
|
||||
set: action((value) => {
|
||||
configrationValue.set(value);
|
||||
configrationEditError.set(undefined);
|
||||
}),
|
||||
error: computed(() => configrationEditError.get()),
|
||||
setError: action((error) => configrationEditError.set(String(error))),
|
||||
};
|
||||
const versionValue = observable.box<HelmChartVersion>(undefined, {
|
||||
deep: false,
|
||||
});
|
||||
const version: UpgradeChartModel["version"] = {
|
||||
value: computed(() => versionValue.get() ?? versions.value.get()[0]),
|
||||
set: action((option) => versionValue.set(option?.value)),
|
||||
};
|
||||
const versionOptions = computed(() => (
|
||||
versions.value
|
||||
.get()
|
||||
.map(version => ({
|
||||
value: version,
|
||||
label: `${version.repo}/${release.getChart()}-${version.version}`,
|
||||
}))
|
||||
));
|
||||
|
||||
return {
|
||||
release,
|
||||
versionOptions,
|
||||
configration,
|
||||
version,
|
||||
submit: async () => {
|
||||
const selectedVersion = version.value.get();
|
||||
|
||||
if (!selectedVersion || configrationEditError.get()) {
|
||||
return {
|
||||
completedSuccessfully: false,
|
||||
};
|
||||
}
|
||||
|
||||
await updateRelease(
|
||||
release.getName(),
|
||||
release.getNs(),
|
||||
{
|
||||
chart: release.getChart(),
|
||||
values: configration.value.get(),
|
||||
...selectedVersion,
|
||||
},
|
||||
);
|
||||
storedConfiguration.invalidate();
|
||||
|
||||
return {
|
||||
completedSuccessfully: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, tab: DockTab) => tab.id,
|
||||
}),
|
||||
});
|
||||
|
||||
export default upgradeChartModelInjectable;
|
||||
@@ -6,26 +6,19 @@
|
||||
import "./upgrade-chart.scss";
|
||||
|
||||
import React from "react";
|
||||
import { action, makeObservable, observable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { observer } from "mobx-react";
|
||||
import { cssNames } from "../../../utils";
|
||||
import type { DockTab } from "../dock/store";
|
||||
import { InfoPanel } from "../info-panel";
|
||||
import type { UpgradeChartTabStore } from "./store";
|
||||
import { Spinner } from "../../spinner";
|
||||
import { Badge } from "../../badge";
|
||||
import { EditorPanel } from "../editor-panel";
|
||||
import { helmChartStore, type ChartVersion } from "../../+helm-charts/helm-chart.store";
|
||||
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import type { SelectOption } from "../../select";
|
||||
import { Select } from "../../select";
|
||||
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import upgradeChartTabStoreInjectable from "./store.injectable";
|
||||
import updateReleaseInjectable from "../../+helm-releases/update-release/update-release.injectable";
|
||||
import releasesInjectable from "../../+helm-releases/releases.injectable";
|
||||
import type { CallForHelmReleaseUpdate } from "../../+helm-releases/update-release/call-for-helm-release-update/call-for-helm-release-update.injectable";
|
||||
import { first } from "lodash/fp";
|
||||
import type { UpgradeChartModel } from "./upgrade-chart-model.injectable";
|
||||
import upgradeChartModelInjectable from "./upgrade-chart-model.injectable";
|
||||
import type { HelmChartVersion } from "../../+helm-charts/helm-charts/versions";
|
||||
|
||||
export interface UpgradeChartProps {
|
||||
className?: string;
|
||||
@@ -33,159 +26,74 @@ export interface UpgradeChartProps {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
releases: IAsyncComputed<HelmRelease[]>;
|
||||
upgradeChartTabStore: UpgradeChartTabStore;
|
||||
updateRelease: CallForHelmReleaseUpdate;
|
||||
model: UpgradeChartModel;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps & Dependencies> {
|
||||
@observable error?: string;
|
||||
@observable versions = observable.array<ChartVersion>();
|
||||
@observable version: ChartVersion | undefined = undefined;
|
||||
|
||||
constructor(props: UpgradeChartProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(
|
||||
() => this.release,
|
||||
release => this.reloadVersions(release),
|
||||
{
|
||||
fireImmediately: true,
|
||||
},
|
||||
),
|
||||
reaction(
|
||||
() => this.release?.getRevision(),
|
||||
() => this.reloadValues(),
|
||||
{
|
||||
fireImmediately: true,
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
return this.props.tab.id;
|
||||
}
|
||||
|
||||
get release() {
|
||||
const tabData = this.props.upgradeChartTabStore.getData(this.tabId);
|
||||
|
||||
if (!tabData) return null;
|
||||
|
||||
return this.props.releases.value.get().find(release => release.getName() === tabData.releaseName);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.props.upgradeChartTabStore.values.getData(this.tabId);
|
||||
}
|
||||
|
||||
async reloadValues() {
|
||||
this.props.upgradeChartTabStore.reloadValues(this.props.tab.id);
|
||||
}
|
||||
|
||||
async reloadVersions(release: HelmRelease | null | undefined) {
|
||||
if (!release) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.version = undefined;
|
||||
this.versions.clear();
|
||||
const versions = await helmChartStore.getVersions(release.getChart());
|
||||
|
||||
this.versions.replace(versions);
|
||||
this.version = first(this.versions);
|
||||
}
|
||||
|
||||
onChange = action((value: string) => {
|
||||
this.error = "";
|
||||
this.props.upgradeChartTabStore.values.setData(this.tabId, value);
|
||||
});
|
||||
|
||||
onError = action((error: Error | string) => {
|
||||
this.error = error.toString();
|
||||
});
|
||||
|
||||
upgrade = async () => {
|
||||
if (this.error || !this.release || !this.version || !this.value) {
|
||||
return null;
|
||||
const { model } = this.props;
|
||||
const { completedSuccessfully } = await model.submit();
|
||||
|
||||
if (completedSuccessfully) {
|
||||
return (
|
||||
<p>
|
||||
{"Release "}
|
||||
<b>{model.release.getName()}</b>
|
||||
{" successfully upgraded to version "}
|
||||
<b>{model.version.value.get()}</b>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const { version, repo } = this.version;
|
||||
const releaseName = this.release.getName();
|
||||
const releaseNs = this.release.getNs();
|
||||
|
||||
await this.props.updateRelease(releaseName, releaseNs, {
|
||||
chart: this.release.getChart(),
|
||||
values: this.value,
|
||||
repo, version,
|
||||
});
|
||||
|
||||
return (
|
||||
<p>
|
||||
{"Release "}
|
||||
<b>{releaseName}</b>
|
||||
{" successfully upgraded to version "}
|
||||
<b>{version}</b>
|
||||
</p>
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tabId, release, value, error, onChange, onError, upgrade, versions, version } = this;
|
||||
const { className } = this.props;
|
||||
|
||||
if (!release || !this.props.upgradeChartTabStore.isReady(tabId) || !version) {
|
||||
return <Spinner center />;
|
||||
}
|
||||
const currentVersion = release.getVersion();
|
||||
const versionOptions = versions.map(version => ({
|
||||
value: version,
|
||||
label: `${version.repo}/${release.getChart()}-${version.version}`,
|
||||
}));
|
||||
const controlsAndInfo = (
|
||||
<div className="upgrade flex gaps align-center">
|
||||
<span>Release</span>
|
||||
{" "}
|
||||
<Badge label={release.getName()} />
|
||||
<span>Namespace</span>
|
||||
{" "}
|
||||
<Badge label={release.getNs()} />
|
||||
<span>Version</span>
|
||||
{" "}
|
||||
<Badge label={currentVersion} />
|
||||
<span>Upgrade version</span>
|
||||
<Select<ChartVersion, SelectOption<ChartVersion>, false>
|
||||
id="char-version-input"
|
||||
className="chart-version"
|
||||
menuPlacement="top"
|
||||
themeName="outlined"
|
||||
value={version}
|
||||
options={versionOptions}
|
||||
onChange={option => this.version = option?.value}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const { model, className, tab } = this.props;
|
||||
const tabId = tab.id;
|
||||
const { release } = model;
|
||||
|
||||
return (
|
||||
<div className={cssNames("UpgradeChart flex column", className)}>
|
||||
<div
|
||||
className={cssNames("UpgradeChart flex column", className)}
|
||||
data-testid={`upgrade-chart-dock-tab-contents-for-${release.getId()}`}
|
||||
>
|
||||
<InfoPanel
|
||||
tabId={tabId}
|
||||
error={error}
|
||||
submit={upgrade}
|
||||
error={model.configration.error.get()}
|
||||
submit={this.upgrade}
|
||||
submitLabel="Upgrade"
|
||||
submittingMessage="Updating.."
|
||||
controls={controlsAndInfo}
|
||||
controls={(
|
||||
<div className="upgrade flex gaps align-center">
|
||||
<span>Release</span>
|
||||
{" "}
|
||||
<Badge label={release.getName()} />
|
||||
<span>Namespace</span>
|
||||
{" "}
|
||||
<Badge label={release.getNs()} />
|
||||
<span>Version</span>
|
||||
{" "}
|
||||
<Badge label={release.getVersion()} />
|
||||
<span>Upgrade version</span>
|
||||
<Select<HelmChartVersion, SelectOption<HelmChartVersion>, false>
|
||||
id="char-version-input"
|
||||
className="chart-version"
|
||||
menuPlacement="top"
|
||||
themeName="outlined"
|
||||
value={model.version.value.get()}
|
||||
options={model.versionOptions.get()}
|
||||
onChange={model.version.set}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={value ?? ""}
|
||||
onChange={onChange}
|
||||
onError={onError}
|
||||
value={model.configration.value.get()}
|
||||
onChange={model.configration.set}
|
||||
onError={model.configration.setError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -193,10 +101,9 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
|
||||
}
|
||||
|
||||
export const UpgradeChart = withInjectables<Dependencies, UpgradeChartProps>(NonInjectedUpgradeChart, {
|
||||
getProps: (di, props) => ({
|
||||
releases: di.inject(releasesInjectable),
|
||||
updateRelease: di.inject(updateReleaseInjectable),
|
||||
upgradeChartTabStore: di.inject(upgradeChartTabStoreInjectable),
|
||||
getPlaceholder: () => <Spinner center />,
|
||||
getProps: async (di, props) => ({
|
||||
...props,
|
||||
model: await di.inject(upgradeChartModelInjectable, props.tab),
|
||||
}),
|
||||
});
|
||||
@@ -5,121 +5,95 @@
|
||||
|
||||
import styles from "./kubeconfig-dialog.module.scss";
|
||||
import React from "react";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import type { IObservableValue } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import yaml from "js-yaml";
|
||||
import type { ServiceAccount } from "../../../common/k8s-api/endpoints";
|
||||
import { cssNames, saveFileDialog } from "../../utils";
|
||||
import { Button } from "../button";
|
||||
import type { DialogProps } from "../dialog";
|
||||
import { Dialog } from "../dialog";
|
||||
import { Icon } from "../icon";
|
||||
import { Notifications } from "../notifications";
|
||||
import type { ShowNotification } from "../notifications";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { apiBase } from "../../api";
|
||||
import { MonacoEditor } from "../monaco-editor";
|
||||
import { clipboard } from "electron";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import showSuccessNotificationInjectable from "../notifications/show-success-notification.injectable";
|
||||
import kubeconfigDialogStateInjectable from "./state.injectable";
|
||||
|
||||
export interface KubeconfigDialogData {
|
||||
title?: React.ReactNode;
|
||||
loader: () => Promise<any>;
|
||||
config: string;
|
||||
}
|
||||
|
||||
export interface KubeConfigDialogProps extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
const dialogState = observable.box<KubeconfigDialogData | undefined>();
|
||||
interface Dependencies {
|
||||
state: IObservableValue<KubeconfigDialogData | undefined>;
|
||||
showSuccessNotification: ShowNotification;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class KubeConfigDialog extends React.Component<KubeConfigDialogProps> {
|
||||
@observable config = ""; // parsed kubeconfig in yaml format
|
||||
|
||||
constructor(props: KubeConfigDialogProps) {
|
||||
class NonInjectedKubeConfigDialog extends React.Component<KubeConfigDialogProps & Dependencies> {
|
||||
constructor(props: KubeConfigDialogProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
static open(data: KubeconfigDialogData) {
|
||||
dialogState.set(data);
|
||||
}
|
||||
|
||||
static close() {
|
||||
dialogState.set(undefined);
|
||||
}
|
||||
|
||||
close = () => {
|
||||
KubeConfigDialog.close();
|
||||
this.props.state.set(undefined);
|
||||
};
|
||||
|
||||
onOpen = (data: KubeconfigDialogData) => {
|
||||
this.loadConfig(data);
|
||||
copyToClipboard = (config: string) => {
|
||||
clipboard.writeText(config);
|
||||
this.props.showSuccessNotification("Config copied to clipboard");
|
||||
};
|
||||
|
||||
async loadConfig(data: KubeconfigDialogData) {
|
||||
const config = await data.loader().catch(err => {
|
||||
Notifications.error(err);
|
||||
this.close();
|
||||
});
|
||||
|
||||
this.config = config ? yaml.dump(config) : "";
|
||||
}
|
||||
|
||||
copyToClipboard = () => {
|
||||
clipboard.writeText(this.config);
|
||||
Notifications.ok("Config copied to clipboard");
|
||||
download = (config: string) => {
|
||||
saveFileDialog("config", config, "text/yaml");
|
||||
};
|
||||
|
||||
download = () => {
|
||||
saveFileDialog("config", this.config, "text/yaml");
|
||||
};
|
||||
|
||||
renderContents(data: KubeconfigDialogData) {
|
||||
const yamlConfig = this.config;
|
||||
|
||||
return (
|
||||
<Wizard header={<h5>{data.title || "Kubeconfig File"}</h5>}>
|
||||
<WizardStep
|
||||
customButtons={(
|
||||
<div className="actions flex gaps">
|
||||
<Button plain onClick={this.copyToClipboard}>
|
||||
<Icon material="assignment"/>
|
||||
{" Copy to clipboard"}
|
||||
</Button>
|
||||
<Button plain onClick={this.download}>
|
||||
<Icon material="cloud_download"/>
|
||||
{" Download file"}
|
||||
</Button>
|
||||
<Button
|
||||
plain
|
||||
className="box right"
|
||||
onClick={this.close}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
prev={this.close}
|
||||
>
|
||||
<MonacoEditor
|
||||
readOnly
|
||||
className={styles.editor}
|
||||
value={yamlConfig}
|
||||
/>
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
);
|
||||
}
|
||||
renderContents = (data: KubeconfigDialogData) => (
|
||||
<Wizard header={<h5>{ data.title || "Kubeconfig File" }</h5>}>
|
||||
<WizardStep
|
||||
customButtons={ (
|
||||
<div className="actions flex gaps">
|
||||
<Button plain onClick={() => this.copyToClipboard(data.config)}>
|
||||
<Icon material="assignment" />
|
||||
{" Copy to clipboard"}
|
||||
</Button>
|
||||
<Button plain onClick={() => this.download(data.config)}>
|
||||
<Icon material="cloud_download" />
|
||||
{" Download file"}
|
||||
</Button>
|
||||
<Button
|
||||
plain
|
||||
className="box right"
|
||||
onClick={this.close}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
) }
|
||||
prev={this.close}
|
||||
>
|
||||
<MonacoEditor
|
||||
readOnly
|
||||
className={styles.editor}
|
||||
value={data.config}
|
||||
/>
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
);
|
||||
|
||||
render() {
|
||||
const { className, ...dialogProps } = this.props;
|
||||
const data = dialogState.get();
|
||||
const { className, state, ...dialogProps } = this.props;
|
||||
const data = state.get();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className={cssNames(styles.KubeConfigDialog, className)}
|
||||
isOpen={Boolean(data)}
|
||||
onOpen={data && (() => this.onOpen(data))}
|
||||
isOpen={!!data}
|
||||
close={this.close}
|
||||
>
|
||||
{data && this.renderContents(data)}
|
||||
@@ -128,12 +102,10 @@ export class KubeConfigDialog extends React.Component<KubeConfigDialogProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export function openServiceAccountKubeConfig(account: ServiceAccount) {
|
||||
const accountName = account.getName();
|
||||
const namespace = account.getNs();
|
||||
|
||||
KubeConfigDialog.open({
|
||||
title: `${accountName} kubeconfig`,
|
||||
loader: () => apiBase.get(`/kubeconfig/service-account/${namespace}/${accountName}`),
|
||||
});
|
||||
}
|
||||
export const KubeConfigDialog = withInjectables<Dependencies, KubeConfigDialogProps>(NonInjectedKubeConfigDialog, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
showSuccessNotification: di.inject(showSuccessNotificationInjectable),
|
||||
state: di.inject(kubeconfigDialogStateInjectable),
|
||||
}),
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 { ServiceAccount } from "../../../common/k8s-api/endpoints";
|
||||
import { urlBuilderFor } from "../../../common/utils/buildUrl";
|
||||
import apiBaseInjectable from "../../k8s/api-base.injectable";
|
||||
import openKubeconfigDialogInjectable from "./open.injectable";
|
||||
|
||||
export type OpenServiceAccountKubeConfigDialog = (account: ServiceAccount) => void;
|
||||
|
||||
const serviceAccountConfigEndpoint = urlBuilderFor("/kubeconfig/service-account/:namespace/:name");
|
||||
|
||||
const openServiceAccountKubeConfigDialogInjectable = getInjectable({
|
||||
id: "open-service-account-kube-config-dialog",
|
||||
instantiate: (di): OpenServiceAccountKubeConfigDialog => {
|
||||
const apiBase = di.inject(apiBaseInjectable);
|
||||
const openKubeconfigDialog = di.inject(openKubeconfigDialogInjectable);
|
||||
|
||||
return (account) => openKubeconfigDialog({
|
||||
title: `${account.getName()} kubeconfig`,
|
||||
loader: () => apiBase.get(serviceAccountConfigEndpoint.compile({
|
||||
name: account.getName(),
|
||||
namespace: account.getNs(),
|
||||
})),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default openServiceAccountKubeConfigDialogInjectable;
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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 React from "react";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import showCheckedErrorNotificationInjectable from "../notifications/show-checked-error.injectable";
|
||||
import kubeconfigDialogStateInjectable from "./state.injectable";
|
||||
|
||||
export interface OpenKubeconfigDialogArgs {
|
||||
title?: React.ReactNode;
|
||||
loader: () => Promise<string>;
|
||||
}
|
||||
|
||||
export type OpenKubeconfigDialog = (openArgs: OpenKubeconfigDialogArgs) => void;
|
||||
|
||||
const openKubeconfigDialogInjectable = getInjectable({
|
||||
id: "open-kubeconfig-dialog",
|
||||
instantiate: (di): OpenKubeconfigDialog => {
|
||||
const state = di.inject(kubeconfigDialogStateInjectable);
|
||||
const showCheckedErrorNotification = di.inject(showCheckedErrorNotificationInjectable);
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
return ({ title, loader }) => {
|
||||
(async () => {
|
||||
try {
|
||||
const config = await loader();
|
||||
|
||||
state.set({ title, config });
|
||||
} catch (error) {
|
||||
showCheckedErrorNotification(error, "Failed to retrive config for dialog");
|
||||
logger.warn("[KUBEOCONFIG-DIALOG]: failed to retrived config for dialog", error);
|
||||
}
|
||||
})();
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default openKubeconfigDialogInjectable;
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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 { observable } from "mobx";
|
||||
import type { KubeconfigDialogData } from "./kubeconfig-dialog";
|
||||
|
||||
const kubeconfigDialogStateInjectable = getInjectable({
|
||||
id: "kubeconfig-dialog-state",
|
||||
instantiate: () => observable.box<KubeconfigDialogData>(),
|
||||
});
|
||||
|
||||
export default kubeconfigDialogStateInjectable;
|
||||
Loaded 100 of 367 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user