mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix installation of helm charts (#5841)
* Relax validator for installing charts Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Tweak spacing between words in confirmation dialog Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add mocks for monaco editor and virtualized auto sizer to allow components to be rendered in unit tests Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Improve typing for a function Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove usage of shared global state from a component Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Provide a way to unit test usages of storages Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add way to get current value from select in behavioural unit tests Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Rework installation of helm charts to get rid of the majority of bugs Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Update snapshots Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove technical test for being covered in behaviours Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Split behaviour to smaller pieces Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Add tests accidentally removed back Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Mark functions causing side effects Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove behaviour covered by other behaviours Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Tweak naming Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> * Remove unused dependency Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
c70788569f
commit
f281df1693
@ -3,3 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
export default {};
|
||||
|
||||
export const Uri = {
|
||||
file: (path: string) => path,
|
||||
};
|
||||
|
||||
export const editor = {
|
||||
getModel: () => ({}),
|
||||
create: () => ({}),
|
||||
};
|
||||
|
||||
17
__mocks__/react-virtualized-auto-sizer.tsx
Normal file
17
__mocks__/react-virtualized-auto-sizer.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
import type { Size } from "react-virtualized-auto-sizer";
|
||||
|
||||
export default ({ children } : { children: (size: Size) => React.ReactNode }) => {
|
||||
return (
|
||||
<div>
|
||||
{children({
|
||||
height: 420000,
|
||||
width: 100,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -472,6 +472,7 @@ exports[`cluster - order of sidebar items when rendered renders 1`] = `
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -1103,6 +1104,7 @@ exports[`cluster - order of sidebar items when rendered when parent is expanded
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
|
||||
@ -441,6 +441,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -983,6 +984,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -1547,6 +1549,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -1992,6 +1995,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -2414,6 +2418,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -2978,6 +2983,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -3520,6 +3526,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
|
||||
@ -441,6 +441,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -980,6 +981,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -1559,6 +1561,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -2058,6 +2061,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -2557,6 +2561,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -3015,6 +3020,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -3594,6 +3600,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -4133,6 +4140,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
|
||||
@ -411,6 +411,7 @@ exports[`cluster - visibility of sidebar items given kube resource for route is
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -967,6 +968,7 @@ exports[`cluster - visibility of sidebar items given kube resource for route is
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
|
||||
@ -293,6 +293,7 @@ exports[`disable-cluster-pages-when-cluster-is-not-relevant given extension shou
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -807,6 +808,7 @@ exports[`disable-cluster-pages-when-cluster-is-not-relevant given extension shou
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -1321,6 +1323,7 @@ exports[`disable-cluster-pages-when-cluster-is-not-relevant given not yet known
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
|
||||
@ -432,6 +432,7 @@ exports[`disable sidebar items when cluster is not relevant given extension shou
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -946,6 +947,7 @@ exports[`disable sidebar items when cluster is not relevant given extension shou
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -1460,6 +1462,7 @@ exports[`disable sidebar items when cluster is not relevant given not yet known
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
|
||||
@ -348,6 +348,7 @@ exports[`disable kube object detail items when cluster is not relevant given ext
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -836,6 +837,7 @@ exports[`disable kube object detail items when cluster is not relevant given ext
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -1324,6 +1326,7 @@ exports[`disable kube object detail items when cluster is not relevant given not
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
|
||||
@ -298,6 +298,7 @@ exports[`disable kube object menu items when cluster is not relevant given exten
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -692,6 +693,7 @@ exports[`disable kube object menu items when cluster is not relevant given exten
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -1086,6 +1088,7 @@ exports[`disable kube object menu items when cluster is not relevant given not y
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
|
||||
@ -300,6 +300,7 @@ exports[`disable kube object statuses when cluster is not relevant given extensi
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -689,6 +690,7 @@ exports[`disable kube object statuses when cluster is not relevant given extensi
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -1078,6 +1080,7 @@ exports[`disable kube object statuses when cluster is not relevant given not yet
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
|
||||
@ -24,6 +24,7 @@ import { navigateToRouteInjectionToken } from "../../common/front-end-routing/na
|
||||
import sidebarStorageInjectable from "../../renderer/components/layout/sidebar-storage/sidebar-storage.injectable";
|
||||
import hostedClusterIdInjectable from "../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
|
||||
import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time";
|
||||
import storageSaveDelayInjectable from "../../renderer/utils/create-storage/storage-save-delay.injectable";
|
||||
|
||||
describe("cluster - sidebar and tab navigation for core", () => {
|
||||
let applicationBuilder: ApplicationBuilder;
|
||||
@ -41,6 +42,8 @@ describe("cluster - sidebar and tab navigation for core", () => {
|
||||
applicationBuilder.beforeApplicationStart(({ rendererDi }) => {
|
||||
rendererDi.override(hostedClusterIdInjectable, () => "some-hosted-cluster-id");
|
||||
|
||||
rendererDi.override(storageSaveDelayInjectable, () => 250);
|
||||
|
||||
rendererDi.override(
|
||||
directoryForLensLocalStorageInjectable,
|
||||
() => "/some-directory-for-lens-local-storage",
|
||||
|
||||
@ -21,6 +21,7 @@ import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-t
|
||||
import { getExtensionFakeFor } from "../../renderer/components/test-utils/get-extension-fake";
|
||||
import type { IObservableValue } from "mobx";
|
||||
import { runInAction, computed, observable } from "mobx";
|
||||
import storageSaveDelayInjectable from "../../renderer/utils/create-storage/storage-save-delay.injectable";
|
||||
|
||||
// TODO: Make tooltips free of side effects by making it deterministic
|
||||
jest.mock("../../renderer/components/tooltip/withTooltip", () => ({
|
||||
@ -43,6 +44,8 @@ describe("cluster - sidebar and tab navigation for extensions", () => {
|
||||
applicationBuilder.beforeApplicationStart(({ rendererDi }) => {
|
||||
rendererDi.override(hostedClusterIdInjectable, () => "some-hosted-cluster-id");
|
||||
|
||||
rendererDi.override(storageSaveDelayInjectable, () => 250);
|
||||
|
||||
rendererDi.override(
|
||||
directoryForLensLocalStorageInjectable,
|
||||
() => "/some-directory-for-lens-local-storage",
|
||||
|
||||
@ -417,6 +417,7 @@ exports[`disable workloads overview details when cluster is not relevant given e
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -931,6 +932,7 @@ exports[`disable workloads overview details when cluster is not relevant given e
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -1445,6 +1447,7 @@ exports[`disable workloads overview details when cluster is not relevant given n
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
|
||||
@ -1,588 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`helm-charts - navigation to Helm charts when navigating to Helm charts renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="Notifications flex column align-flex-end"
|
||||
/>
|
||||
<div
|
||||
class="mainLayout"
|
||||
style="--sidebar-width: 200px;"
|
||||
>
|
||||
<div
|
||||
class="sidebar"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col"
|
||||
data-testid="cluster-sidebar"
|
||||
>
|
||||
<div
|
||||
class="SidebarCluster"
|
||||
>
|
||||
<div
|
||||
class="Avatar rounded loadingAvatar"
|
||||
style="width: 40px; height: 40px;"
|
||||
>
|
||||
??
|
||||
</div>
|
||||
<div
|
||||
class="loadingClusterName"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="sidebarNav sidebar-active-status"
|
||||
>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-is-active-test="false"
|
||||
data-testid="sidebar-item-workloads"
|
||||
>
|
||||
<a
|
||||
class="nav-item flex gaps align-center expandable"
|
||||
data-testid="sidebar-item-link-for-workloads"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon svg focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
/>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Workloads
|
||||
</span>
|
||||
<i
|
||||
class="Icon expand-icon box right material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="keyboard_arrow_down"
|
||||
>
|
||||
keyboard_arrow_down
|
||||
</span>
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-is-active-test="false"
|
||||
data-testid="sidebar-item-config"
|
||||
>
|
||||
<a
|
||||
class="nav-item flex gaps align-center"
|
||||
data-testid="sidebar-item-link-for-config"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="list"
|
||||
>
|
||||
list
|
||||
</span>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Config
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-is-active-test="false"
|
||||
data-testid="sidebar-item-network"
|
||||
>
|
||||
<a
|
||||
class="nav-item flex gaps align-center expandable"
|
||||
data-testid="sidebar-item-link-for-network"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="device_hub"
|
||||
>
|
||||
device_hub
|
||||
</span>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Network
|
||||
</span>
|
||||
<i
|
||||
class="Icon expand-icon box right material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="keyboard_arrow_down"
|
||||
>
|
||||
keyboard_arrow_down
|
||||
</span>
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-is-active-test="false"
|
||||
data-testid="sidebar-item-storage"
|
||||
>
|
||||
<a
|
||||
class="nav-item flex gaps align-center"
|
||||
data-testid="sidebar-item-link-for-storage"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="storage"
|
||||
>
|
||||
storage
|
||||
</span>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Storage
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-is-active-test="true"
|
||||
data-testid="sidebar-item-helm"
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
class="nav-item flex gaps align-center expandable active"
|
||||
data-testid="sidebar-item-link-for-helm"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon svg focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
/>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Helm
|
||||
</span>
|
||||
<i
|
||||
class="Icon expand-icon box right material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="keyboard_arrow_down"
|
||||
>
|
||||
keyboard_arrow_down
|
||||
</span>
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-is-active-test="false"
|
||||
data-testid="sidebar-item-user-management"
|
||||
>
|
||||
<a
|
||||
class="nav-item flex gaps align-center"
|
||||
data-testid="sidebar-item-link-for-user-management"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="security"
|
||||
>
|
||||
security
|
||||
</span>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Access Control
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-is-active-test="false"
|
||||
data-testid="sidebar-item-custom-resources"
|
||||
>
|
||||
<a
|
||||
class="nav-item flex gaps align-center expandable"
|
||||
data-testid="sidebar-item-link-for-custom-resources"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="extension"
|
||||
>
|
||||
extension
|
||||
</span>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Custom Resources
|
||||
</span>
|
||||
<i
|
||||
class="Icon expand-icon box right material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="keyboard_arrow_down"
|
||||
>
|
||||
keyboard_arrow_down
|
||||
</span>
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ResizingAnchor horizontal trailing"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="contents"
|
||||
>
|
||||
<div
|
||||
class="TabLayout"
|
||||
data-testid="tab-layout"
|
||||
>
|
||||
<div
|
||||
class="Tabs center scrollable"
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center active"
|
||||
data-is-active-test="true"
|
||||
data-testid="tab-link-for-charts"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="label"
|
||||
>
|
||||
Charts
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Tab flex gaps align-center"
|
||||
data-is-active-test="false"
|
||||
data-testid="tab-link-for-releases"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="label"
|
||||
>
|
||||
Releases
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main>
|
||||
<div
|
||||
data-testid="page-for-helm-charts"
|
||||
style="display: none;"
|
||||
/>
|
||||
<div
|
||||
class="ItemListLayout flex column HelmCharts"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
>
|
||||
<div
|
||||
class="Input SearchInput focused"
|
||||
>
|
||||
<label
|
||||
class="input-area flex gaps align-center"
|
||||
id=""
|
||||
>
|
||||
<input
|
||||
class="input box grow"
|
||||
placeholder="Search Helm Charts..."
|
||||
spellcheck="false"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
class="Icon material focusable small"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="search"
|
||||
>
|
||||
search
|
||||
</span>
|
||||
</i>
|
||||
</label>
|
||||
<div
|
||||
class="input-info flex gaps"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="items box grow flex column"
|
||||
>
|
||||
<div
|
||||
class="Table flex column HelmCharts box grow dark selectable scrollable sortable autoSize virtual"
|
||||
>
|
||||
<div
|
||||
class="TableHead sticky nowrap topLine"
|
||||
>
|
||||
<div
|
||||
class="TableCell icon nowrap"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="TableCell name nowrap sorting"
|
||||
id="name"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
Name
|
||||
</div>
|
||||
<i
|
||||
class="Icon sortIcon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="arrow_drop_down"
|
||||
>
|
||||
arrow_drop_down
|
||||
</span>
|
||||
</i>
|
||||
</div>
|
||||
<div
|
||||
class="TableCell description nowrap"
|
||||
id="description"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
Description
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableCell version nowrap"
|
||||
id="version"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
Version
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableCell app-version nowrap"
|
||||
id="app-version"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
App Version
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableCell repository nowrap sorting"
|
||||
id="repo"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
Repository
|
||||
</div>
|
||||
<i
|
||||
class="Icon sortIcon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="arrow_drop_down"
|
||||
>
|
||||
arrow_drop_down
|
||||
</span>
|
||||
</i>
|
||||
</div>
|
||||
<div
|
||||
class="TableCell menu nowrap"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
id="menu-actions-for-item-object-list-content"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="more_vert"
|
||||
>
|
||||
more_vert
|
||||
</span>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="NoItems flex box grow"
|
||||
>
|
||||
<div
|
||||
class="box center"
|
||||
>
|
||||
Item list is empty
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="AddRemoveButtons flex gaps"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="footer"
|
||||
>
|
||||
<div
|
||||
class="Dock"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="ResizingAnchor vertical leading"
|
||||
/>
|
||||
<div
|
||||
class="tabs-container flex align-center"
|
||||
>
|
||||
<div
|
||||
class="dockTabs"
|
||||
role="tablist"
|
||||
>
|
||||
<div
|
||||
class="Tabs tabs"
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="terminal"
|
||||
>
|
||||
terminal
|
||||
</span>
|
||||
</i>
|
||||
<div
|
||||
class="label"
|
||||
>
|
||||
<div
|
||||
class="flex align-center"
|
||||
>
|
||||
<span
|
||||
class="title"
|
||||
>
|
||||
Terminal
|
||||
</span>
|
||||
<div
|
||||
class="close"
|
||||
>
|
||||
<i
|
||||
class="Icon material interactive focusable small"
|
||||
id="tooltip_target_13"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="close"
|
||||
>
|
||||
close
|
||||
</span>
|
||||
<div />
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="toolbar flex gaps align-center box grow"
|
||||
>
|
||||
<div
|
||||
class="dock-menu box grow"
|
||||
>
|
||||
<i
|
||||
class="Icon new-dock-tab material interactive focusable"
|
||||
id="menu-actions-for-dock"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="add"
|
||||
>
|
||||
add
|
||||
</span>
|
||||
<div />
|
||||
</i>
|
||||
</div>
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
id="tooltip_target_15"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="fullscreen"
|
||||
>
|
||||
fullscreen
|
||||
</span>
|
||||
<div />
|
||||
</i>
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
id="tooltip_target_16"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="keyboard_arrow_up"
|
||||
>
|
||||
keyboard_arrow_up
|
||||
</span>
|
||||
<div />
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,260 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import type { RenderResult } from "@testing-library/react";
|
||||
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import getRandomInstallChartTabIdInjectable from "../../../renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable";
|
||||
import type { CallForHelmChartValues } from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable";
|
||||
import callForHelmChartValuesInjectable from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable";
|
||||
import namespaceStoreInjectable from "../../../renderer/components/+namespaces/store.injectable";
|
||||
import type { NamespaceStore } from "../../../renderer/components/+namespaces/store";
|
||||
import type { CallForHelmChartVersions } from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
|
||||
import callForHelmChartVersionsInjectable from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
|
||||
import { overrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes";
|
||||
import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
|
||||
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||
import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
|
||||
import { TabKind } from "../../../renderer/components/dock/dock/store";
|
||||
import { controlWhenStoragesAreReady } from "../../../renderer/utils/create-storage/storages-are-ready";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import callForCreateHelmReleaseInjectable from "../../../renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable";
|
||||
|
||||
// TODO: Make tooltips free of side effects by making it deterministic
|
||||
jest.mock("../../../renderer/components/tooltip/withTooltip", () => ({
|
||||
withTooltip: (target: any) => target,
|
||||
}));
|
||||
|
||||
describe("installing helm chart from previously opened tab", () => {
|
||||
let builder: ApplicationBuilder;
|
||||
let rendererDi: DiContainer;
|
||||
let callForHelmChartVersionsMock: AsyncFnMock<CallForHelmChartVersions>;
|
||||
let callForHelmChartValuesMock: AsyncFnMock<CallForHelmChartValues>;
|
||||
let storagesAreReady: () => Promise<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
builder = getApplicationBuilder();
|
||||
|
||||
rendererDi = builder.dis.rendererDi;
|
||||
|
||||
overrideFsWithFakes(rendererDi);
|
||||
|
||||
callForHelmChartVersionsMock = asyncFn();
|
||||
callForHelmChartValuesMock = asyncFn();
|
||||
|
||||
builder.beforeApplicationStart(({ rendererDi }) => {
|
||||
rendererDi.override(
|
||||
directoryForLensLocalStorageInjectable,
|
||||
() => "/some-directory-for-lens-local-storage",
|
||||
);
|
||||
|
||||
rendererDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
|
||||
|
||||
storagesAreReady = controlWhenStoragesAreReady(rendererDi);
|
||||
|
||||
rendererDi.override(
|
||||
callForHelmChartVersionsInjectable,
|
||||
() => callForHelmChartVersionsMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
callForHelmChartValuesInjectable,
|
||||
() => callForHelmChartValuesMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
callForHelmChartValuesInjectable,
|
||||
() => callForHelmChartValuesMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
callForCreateHelmReleaseInjectable,
|
||||
() => jest.fn(),
|
||||
);
|
||||
|
||||
// TODO: Replace store mocking with mock for the actual side-effect (where the namespaces are coming from)
|
||||
rendererDi.override(
|
||||
namespaceStoreInjectable,
|
||||
() =>
|
||||
({
|
||||
contextNamespaces: [],
|
||||
items: [
|
||||
{ getName: () => "default" },
|
||||
{ getName: () => "some-other-namespace" },
|
||||
],
|
||||
selectNamespaces: () => {},
|
||||
} as unknown as NamespaceStore),
|
||||
);
|
||||
|
||||
rendererDi.override(getRandomInstallChartTabIdInjectable, () =>
|
||||
jest
|
||||
.fn(() => "some-irrelevant-tab-id")
|
||||
.mockReturnValueOnce("some-first-tab-id"),
|
||||
);
|
||||
});
|
||||
|
||||
builder.setEnvironmentToClusterFrame();
|
||||
});
|
||||
|
||||
describe("given tab for installing chart was previously opened, when application is started", () => {
|
||||
let rendered: RenderResult;
|
||||
|
||||
beforeEach(async () => {
|
||||
const writeJsonFile = rendererDi.inject(writeJsonFileInjectable);
|
||||
|
||||
writeJsonFile(
|
||||
"/some-directory-for-lens-local-storage/some-cluster-id.json",
|
||||
{
|
||||
dock: {
|
||||
height: 300,
|
||||
tabs: [
|
||||
{
|
||||
id: "some-first-tab-id",
|
||||
kind: TabKind.INSTALL_CHART,
|
||||
title: "Helm Install: some-repository/some-name",
|
||||
pinned: false,
|
||||
},
|
||||
],
|
||||
|
||||
isOpen: true,
|
||||
},
|
||||
|
||||
install_charts: {
|
||||
"some-first-tab-id": {
|
||||
name: "some-name",
|
||||
repo: "some-repository",
|
||||
version: "some-other-version",
|
||||
values: "some-stored-configuration",
|
||||
releaseName: "some-stored-custom-name",
|
||||
namespace: "some-other-namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
rendered = await builder.render();
|
||||
|
||||
await storagesAreReady();
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("still has the dock tab for installing chart", () => {
|
||||
expect(
|
||||
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows dock tab for installing the chart", () => {
|
||||
expect(
|
||||
rendered.getByTestId("dock-tab-content-for-some-first-tab-id"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows spinner in dock tab", () => {
|
||||
expect(
|
||||
rendered.getByTestId("install-chart-tab-spinner"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls for default configuration of the chart", () => {
|
||||
expect(callForHelmChartValuesMock).toHaveBeenCalledWith(
|
||||
"some-repository",
|
||||
"some-name",
|
||||
"some-other-version",
|
||||
);
|
||||
});
|
||||
|
||||
it("calls for available versions", () => {
|
||||
expect(callForHelmChartVersionsMock).toHaveBeenCalledWith(
|
||||
"some-repository",
|
||||
"some-name",
|
||||
);
|
||||
});
|
||||
|
||||
describe("when configuration and version resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await callForHelmChartValuesMock.resolve(
|
||||
"some-default-configuration",
|
||||
);
|
||||
|
||||
await callForHelmChartVersionsMock.resolve([
|
||||
HelmChart.create({
|
||||
apiVersion: "some-api-version",
|
||||
name: "some-name",
|
||||
version: "some-version",
|
||||
repo: "some-repository",
|
||||
created: "2015-10-21T07:28:00Z",
|
||||
description: "some-description",
|
||||
keywords: [],
|
||||
sources: [],
|
||||
urls: [],
|
||||
annotations: {},
|
||||
dependencies: [],
|
||||
maintainers: [],
|
||||
deprecated: false,
|
||||
}),
|
||||
|
||||
HelmChart.create({
|
||||
apiVersion: "some-api-version",
|
||||
name: "some-name",
|
||||
version: "some-other-version",
|
||||
repo: "some-repository",
|
||||
created: "2015-10-21T07:28:00Z",
|
||||
description: "some-description",
|
||||
keywords: [],
|
||||
sources: [],
|
||||
urls: [],
|
||||
annotations: {},
|
||||
dependencies: [],
|
||||
maintainers: [],
|
||||
deprecated: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("has the stored configuration", () => {
|
||||
const input = rendered.getByTestId(
|
||||
"monaco-editor-for-some-first-tab-id",
|
||||
);
|
||||
|
||||
expect(input).toHaveValue("some-stored-configuration");
|
||||
});
|
||||
|
||||
it("has the stored custom name", () => {
|
||||
const input = rendered.getByTestId(
|
||||
"install-chart-custom-name-input-for-some-first-tab-id",
|
||||
);
|
||||
|
||||
expect(input).toHaveValue("some-stored-custom-name");
|
||||
});
|
||||
|
||||
it("has the stored version", () => {
|
||||
const actual = builder.select.getValue(
|
||||
"install-chart-version-select-for-some-first-tab-id",
|
||||
);
|
||||
|
||||
expect(actual).toBe("some-other-version");
|
||||
|
||||
});
|
||||
|
||||
it("has the stored namespace", () => {
|
||||
const actual = builder.select.getValue(
|
||||
"install-chart-namespace-select-for-some-first-tab-id",
|
||||
);
|
||||
|
||||
expect(actual).toBe("some-other-namespace");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,361 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import type { RenderResult } from "@testing-library/react";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
import type { CallForHelmCharts } from "../../../renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable";
|
||||
import callForHelmChartsInjectable from "../../../renderer/components/+helm-charts/helm-charts/call-for-helm-charts.injectable";
|
||||
import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import getRandomInstallChartTabIdInjectable from "../../../renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable";
|
||||
import callForHelmChartValuesInjectable from "../../../renderer/components/dock/install-chart/chart-data/call-for-helm-chart-values.injectable";
|
||||
import callForCreateHelmReleaseInjectable from "../../../renderer/components/+helm-releases/create-release/call-for-create-helm-release.injectable";
|
||||
import type { CallForHelmChartReadme } from "../../../renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable";
|
||||
import callForHelmChartReadmeInjectable from "../../../renderer/components/+helm-charts/details/readme/call-for-helm-chart-readme.injectable";
|
||||
import type { CallForHelmChartVersions } from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
|
||||
import callForHelmChartVersionsInjectable from "../../../renderer/components/+helm-charts/details/versions/call-for-helm-chart-versions.injectable";
|
||||
import { flushPromises } from "../../../common/test-utils/flush-promises";
|
||||
import { overrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes";
|
||||
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||
import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
|
||||
import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
|
||||
// TODO: Make tooltips free of side effects by making it deterministic
|
||||
jest.mock("../../../renderer/components/tooltip/withTooltip", () => ({
|
||||
withTooltip: (target: any) => target,
|
||||
}));
|
||||
|
||||
describe("opening dock tab for installing helm chart", () => {
|
||||
let builder: ApplicationBuilder;
|
||||
let rendererDi: DiContainer;
|
||||
let callForHelmChartsMock: AsyncFnMock<CallForHelmCharts>;
|
||||
let callForHelmChartVersionsMock: AsyncFnMock<CallForHelmChartVersions>;
|
||||
let callForHelmChartReadmeMock: AsyncFnMock<CallForHelmChartReadme>;
|
||||
let callForHelmChartValuesMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
builder = getApplicationBuilder();
|
||||
|
||||
rendererDi = builder.dis.rendererDi;
|
||||
|
||||
overrideFsWithFakes(rendererDi);
|
||||
|
||||
callForHelmChartsMock = asyncFn();
|
||||
callForHelmChartVersionsMock = asyncFn();
|
||||
callForHelmChartReadmeMock = asyncFn();
|
||||
callForHelmChartValuesMock = jest.fn();
|
||||
|
||||
builder.beforeApplicationStart(({ rendererDi }) => {
|
||||
rendererDi.override(
|
||||
directoryForLensLocalStorageInjectable,
|
||||
() => "/some-directory-for-lens-local-storage",
|
||||
);
|
||||
|
||||
rendererDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
|
||||
|
||||
rendererDi.override(
|
||||
callForHelmChartsInjectable,
|
||||
() => callForHelmChartsMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
callForHelmChartVersionsInjectable,
|
||||
() => callForHelmChartVersionsMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
callForHelmChartReadmeInjectable,
|
||||
() => callForHelmChartReadmeMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
callForHelmChartValuesInjectable,
|
||||
() => callForHelmChartValuesMock,
|
||||
);
|
||||
|
||||
rendererDi.override(
|
||||
callForCreateHelmReleaseInjectable,
|
||||
() => jest.fn(),
|
||||
);
|
||||
|
||||
rendererDi.override(getRandomInstallChartTabIdInjectable, () =>
|
||||
jest
|
||||
.fn(() => "some-irrelevant-tab-id")
|
||||
.mockReturnValueOnce("some-tab-id"),
|
||||
);
|
||||
});
|
||||
|
||||
builder.setEnvironmentToClusterFrame();
|
||||
});
|
||||
|
||||
describe("given application is started, when navigating to helm charts", () => {
|
||||
let rendered: RenderResult;
|
||||
|
||||
beforeEach(async () => {
|
||||
rendered = await builder.render();
|
||||
|
||||
builder.helmCharts.navigate();
|
||||
|
||||
const dockStore = rendererDi.inject(dockStoreInjectable);
|
||||
|
||||
// TODO: Make TerminalWindow unit testable to allow realistic behaviour
|
||||
dockStore.closeTab("terminal");
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls for charts", () => {
|
||||
expect(callForHelmChartsMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when charts resolve", () => {
|
||||
beforeEach(async () => {
|
||||
await callForHelmChartsMock.resolve([
|
||||
HelmChart.create({
|
||||
apiVersion: "some-api-version",
|
||||
name: "some-name",
|
||||
version: "some-version",
|
||||
repo: "some-repository",
|
||||
created: "2015-10-21T07:28:00Z",
|
||||
description: "some-description",
|
||||
keywords: [],
|
||||
sources: [],
|
||||
urls: [],
|
||||
annotations: {},
|
||||
dependencies: [],
|
||||
maintainers: [],
|
||||
deprecated: false,
|
||||
}),
|
||||
|
||||
HelmChart.create({
|
||||
apiVersion: "some-api-version",
|
||||
name: "some-other-name",
|
||||
version: "some-version",
|
||||
repo: "some-repository",
|
||||
created: "2015-10-21T07:28:00Z",
|
||||
description: "some-description",
|
||||
keywords: [],
|
||||
sources: [],
|
||||
urls: [],
|
||||
annotations: {},
|
||||
dependencies: [],
|
||||
maintainers: [],
|
||||
deprecated: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when opening details of a chart", () => {
|
||||
beforeEach(() => {
|
||||
const row = rendered.getByTestId(
|
||||
"helm-chart-row-for-some-repository-some-name",
|
||||
);
|
||||
|
||||
fireEvent.click(row);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls for chart versions", () => {
|
||||
expect(callForHelmChartVersionsMock).toHaveBeenCalledWith(
|
||||
"some-repository",
|
||||
"some-name",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows spinner", () => {
|
||||
expect(
|
||||
rendered.getByTestId("spinner-for-chart-details"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when chart versions resolve", () => {
|
||||
beforeEach(async () => {
|
||||
await callForHelmChartVersionsMock.resolve([
|
||||
HelmChart.create({
|
||||
apiVersion: "some-api-version",
|
||||
name: "some-name",
|
||||
version: "some-version",
|
||||
repo: "some-repository",
|
||||
created: "2015-10-21T07:28:00Z",
|
||||
description: "some-description",
|
||||
keywords: [],
|
||||
sources: [],
|
||||
urls: [],
|
||||
annotations: {},
|
||||
dependencies: [],
|
||||
maintainers: [],
|
||||
deprecated: false,
|
||||
}),
|
||||
|
||||
HelmChart.create({
|
||||
apiVersion: "some-api-version",
|
||||
name: "some-name",
|
||||
version: "some-other-version",
|
||||
repo: "some-repository",
|
||||
created: "2015-10-21T07:28:00Z",
|
||||
description: "some-description",
|
||||
keywords: [],
|
||||
sources: [],
|
||||
urls: [],
|
||||
annotations: {},
|
||||
dependencies: [],
|
||||
maintainers: [],
|
||||
deprecated: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("calls for chart readme for the version", () => {
|
||||
expect(callForHelmChartReadmeMock).toHaveBeenCalledWith(
|
||||
"some-repository",
|
||||
"some-name",
|
||||
"some-version",
|
||||
);
|
||||
});
|
||||
|
||||
it("has the latest version as selected", () => {
|
||||
const actual = builder.select.getValue(
|
||||
"helm-chart-version-selector-some-repository-some-name",
|
||||
);
|
||||
|
||||
expect(actual).toBe("some-version");
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("does not shows spinner for details", () => {
|
||||
expect(
|
||||
rendered.queryByTestId("spinner-for-chart-details"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows spinner for readme", () => {
|
||||
expect(
|
||||
rendered.getByTestId("spinner-for-chart-readme"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when readme resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await callForHelmChartReadmeMock.resolve("some-readme");
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("does not show spinner anymore", () => {
|
||||
expect(
|
||||
rendered.queryByTestId("spinner-for-chart-readme"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when selecting different version", () => {
|
||||
beforeEach(() => {
|
||||
callForHelmChartReadmeMock.mockClear();
|
||||
|
||||
builder.select
|
||||
.openMenu(
|
||||
"helm-chart-version-selector-some-repository-some-name",
|
||||
)
|
||||
.selectOption("some-other-version");
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("selects the version", () => {
|
||||
const actual = builder.select.getValue(
|
||||
"helm-chart-version-selector-some-repository-some-name",
|
||||
);
|
||||
|
||||
expect(actual).toBe("some-other-version");
|
||||
});
|
||||
|
||||
it("calls for chart readme for the version", () => {
|
||||
expect(callForHelmChartReadmeMock).toHaveBeenCalledWith(
|
||||
"some-repository",
|
||||
"some-name",
|
||||
"some-other-version",
|
||||
);
|
||||
});
|
||||
|
||||
describe("when readme resolves", () => {
|
||||
beforeEach(async () => {
|
||||
await callForHelmChartReadmeMock.resolve("some-readme");
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("when selecting to install chart, calls for the default configuration of the chart with specific version", async () => {
|
||||
const installButton = rendered.getByTestId(
|
||||
"install-chart-for-some-repository-some-name",
|
||||
);
|
||||
|
||||
fireEvent.click(installButton);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(callForHelmChartValuesMock).toHaveBeenCalledWith(
|
||||
"some-repository",
|
||||
"some-name",
|
||||
"some-other-version",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when selecting to install the chart", () => {
|
||||
beforeEach(() => {
|
||||
callForHelmChartVersionsMock.mockClear();
|
||||
|
||||
const installButton = rendered.getByTestId(
|
||||
"install-chart-for-some-repository-some-name",
|
||||
);
|
||||
|
||||
fireEvent.click(installButton);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("has the dock tab for installing chart", () => {
|
||||
expect(
|
||||
rendered.getByTestId("dock-tab-for-some-tab-id"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows dock tab for installing chart", () => {
|
||||
expect(
|
||||
rendered.getByTestId(
|
||||
"dock-tab-content-for-some-tab-id",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { RenderResult } from "@testing-library/react";
|
||||
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
|
||||
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
|
||||
|
||||
describe("helm-charts - navigation to Helm charts", () => {
|
||||
let applicationBuilder: ApplicationBuilder;
|
||||
|
||||
beforeEach(() => {
|
||||
applicationBuilder = getApplicationBuilder();
|
||||
});
|
||||
|
||||
describe("when navigating to Helm charts", () => {
|
||||
let rendered: RenderResult;
|
||||
|
||||
beforeEach(async () => {
|
||||
applicationBuilder.setEnvironmentToClusterFrame();
|
||||
|
||||
rendered = await applicationBuilder.render();
|
||||
|
||||
applicationBuilder.helmCharts.navigate();
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows page for Helm charts", () => {
|
||||
const page = rendered.getByTestId("page-for-helm-charts");
|
||||
|
||||
expect(page).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -7,7 +7,7 @@ import { v4 as getRandomId } from "uuid";
|
||||
|
||||
const getRandomIdInjectable = getInjectable({
|
||||
id: "get-random-id",
|
||||
instantiate: () => getRandomId,
|
||||
instantiate: () => () => getRandomId(),
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
|
||||
@ -36,3 +36,11 @@ process.on("unhandledRejection", (err: any) => {
|
||||
|
||||
global.TextEncoder = TextEncoder;
|
||||
global.TextDecoder = TextDecoderNode as unknown as typeof TextDecoder;
|
||||
|
||||
global.ResizeObserver = class {
|
||||
observe = () => {};
|
||||
unobserve = () => {};
|
||||
disconnect = () => {};
|
||||
};
|
||||
|
||||
jest.mock("./renderer/components/monaco-editor/monaco-editor");
|
||||
|
||||
@ -18,8 +18,7 @@ const installChartArgsValidator = Joi.object<InstallChartArgs, true, InstallChar
|
||||
.required()
|
||||
.unknown(true),
|
||||
name: Joi
|
||||
.string()
|
||||
.required(),
|
||||
.string(),
|
||||
namespace: Joi
|
||||
.string()
|
||||
.required(),
|
||||
|
||||
@ -1,423 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<HelmChartDetails /> before getChartDetails resolves renders 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="Notifications flex column align-flex-end"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="Animate slide-right Drawer HelmChartDetails right enter"
|
||||
style="--size: 725px; --enter-duration: 100ms; --leave-duration: 100ms;"
|
||||
>
|
||||
<div
|
||||
class="drawer-wrapper flex column"
|
||||
>
|
||||
<div
|
||||
class="drawer-title flex align-center"
|
||||
>
|
||||
<div
|
||||
class="drawer-title-text flex gaps align-center"
|
||||
>
|
||||
Chart: a galaxy far far away/a name
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
id="tooltip_target_1"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="content_copy"
|
||||
>
|
||||
content_copy
|
||||
</span>
|
||||
<div />
|
||||
</i>
|
||||
</div>
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
id="tooltip_target_2"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="close"
|
||||
>
|
||||
close
|
||||
</span>
|
||||
<div />
|
||||
</i>
|
||||
</div>
|
||||
<div
|
||||
class="drawer-content flex column box grow"
|
||||
>
|
||||
<div
|
||||
class="Spinner singleColor center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ResizingAnchor horizontal leading"
|
||||
/>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`<HelmChartDetails /> before getChartDetails resolves when getChartDetails resolves with one version renders 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="Notifications flex column align-flex-end"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="Animate slide-right Drawer HelmChartDetails right enter"
|
||||
style="--size: 725px; --enter-duration: 100ms; --leave-duration: 100ms;"
|
||||
>
|
||||
<div
|
||||
class="drawer-wrapper flex column"
|
||||
>
|
||||
<div
|
||||
class="drawer-title flex align-center"
|
||||
>
|
||||
<div
|
||||
class="drawer-title-text flex gaps align-center"
|
||||
>
|
||||
Chart: a galaxy far far away/a name
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
id="tooltip_target_3"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="content_copy"
|
||||
>
|
||||
content_copy
|
||||
</span>
|
||||
<div />
|
||||
</i>
|
||||
</div>
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
id="tooltip_target_4"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="close"
|
||||
>
|
||||
close
|
||||
</span>
|
||||
<div />
|
||||
</i>
|
||||
</div>
|
||||
<div
|
||||
class="drawer-content flex column box grow"
|
||||
>
|
||||
<div
|
||||
class="box grow"
|
||||
>
|
||||
<div
|
||||
class="introduction flex align-flex-start"
|
||||
>
|
||||
<div
|
||||
class="intro-logo"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 722.8 702"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="m318 299.5c2.1 1.6 4.8 2.5 7.6 2.5 6.9 0 12.6-5.5 12.9-12.3l.3-.2 4.3-76.7c-5.2.6-10.4 1.5-15.6 2.7-28.5 6.5-53.2 20.5-72.6 39.5l62.9 44.6z"
|
||||
/>
|
||||
<path
|
||||
d="m309.5 411.9c-1.4-5.9-6.6-9.9-12.4-10-.8 0-1.7.1-2.5.2l-.1-.2-75.5 12.8c11.7 32.2 33.4 58.5 60.8 76.1l29.2-70.7-.2-.3c1.1-2.4 1.4-5.2.7-7.9z"
|
||||
/>
|
||||
<path
|
||||
d="m284.4 357.5c2.5-.7 4.9-2.2 6.7-4.4 4.3-5.4 3.6-13.2-1.6-17.8l.1-.3-57.4-51.4c-17 27.8-25.1 61.1-21.4 95.3l73.6-21.2z"
|
||||
/>
|
||||
<path
|
||||
d="m340.2 380 21.2 10.2 21.1-10.1 5.3-22.9-14.6-18.2h-23.6l-14.6 18.2z"
|
||||
/>
|
||||
<path
|
||||
d="m384.2 289.4c.1 2.6 1 5.2 2.8 7.5 4.3 5.4 12.1 6.4 17.7 2.4l.2.1 62.5-44.3c-23.6-23.1-54.4-38.2-87.6-42.2z"
|
||||
/>
|
||||
<path
|
||||
d="m490.3 283.7-57.1 51.1v.2c-2 1.7-3.5 4.1-4.1 6.8-1.5 6.8 2.5 13.5 9.2 15.3l.1.3 74 21.3c1.6-16 .6-32.5-3.2-49-3.9-16.8-10.4-32.2-18.9-46z"
|
||||
/>
|
||||
<path
|
||||
d="m372.8 439.6c-1.2-2.3-3.2-4.3-5.8-5.5-2-.9-4-1.4-6-1.3-4.5.2-8.7 2.6-10.9 6.8h-.1l-37.1 67.1c25.7 8.8 54.1 10.7 82.5 4.2 5.1-1.2 10-2.5 14.9-4.2l-37.3-67.1z"
|
||||
/>
|
||||
<path
|
||||
d="m711.7 425-60.4-262.2c-3.2-13.7-12.5-25.3-25.3-31.4l-244.4-116.8c-7.1-3.4-14.8-4.9-22.7-4.5-6.2.3-12.3 1.9-17.9 4.5l-244.3 116.7c-12.8 6.1-22.1 17.7-25.3 31.4l-60.2 262.3c-2.8 12.2-.5 25 6.3 35.5.8 1.3 1.7 2.5 2.7 3.7l169.1 210.3c8.9 11 22.3 17.4 36.5 17.4l271.2-.1c14.2 0 27.7-6.4 36.5-17.4l169.1-210.3c8.9-10.9 12.2-25.4 9.1-39.1zm-93-3.2c-1.8 7.8-10.2 12.6-18.9 10.7-.1 0-.2 0-.2 0-.1 0-.2-.1-.3-.1-1.2-.3-2.7-.5-3.8-.8-5-1.3-8.6-3.3-13.1-5.1-9.7-3.5-17.7-6.4-25.5-7.5-4-.3-6 1.6-8.2 3-1.1-.2-4.4-.8-6.2-1.1-14 44-43.9 82.2-84.3 106.1.7 1.7 1.9 5.3 2.4 5.9-.9 2.5-2.3 4.8-1.1 8.6 2.8 7.4 7.4 14.6 13 23.2 2.7 4 5.4 7.1 7.8 11.7.6 1.1 1.3 2.8 1.9 3.9 3.8 8 1 17.3-6.2 20.8-7.3 3.5-16.3-.2-20.2-8.3-.6-1.1-1.3-2.7-1.8-3.8-2.1-4.7-2.8-8.8-4.2-13.4-3.3-9.7-6-17.8-10-24.6-2.2-3.3-5-3.7-7.5-4.5-.5-.8-2.2-4-3.1-5.6-8.1 3.1-16.4 5.6-25.1 7.6-37.9 8.6-75.9 5.1-109.9-7.9l-3.3 6c-2.5.7-4.8 1.3-6.3 3.1-5.3 6.4-7.5 16.6-11.3 26.3-1.5 4.6-2.1 8.7-4.2 13.4-.5 1.1-1.3 2.6-1.8 3.7-3.9 8.1-12.9 11.7-20.2 8.2-7.2-3.5-10-12.7-6.2-20.8.6-1.2 1.3-2.8 1.9-3.9 2.4-4.6 5.2-7.7 7.8-11.7 5.5-8.7 10.4-16.4 13.2-23.8.7-2.4-.3-5.8-1.3-8.3l2.7-6.4c-38.9-23.1-69.7-59.8-84.3-105.3l-6.4 1.1c-1.7-1-5.1-3.2-8.4-3-7.8 1.1-15.8 4-25.5 7.5-4.5 1.7-8.1 3.7-13.1 5-1.1.3-2.6.6-3.8.8-.1 0-.2.1-.3.1s-.2 0-.2 0c-8.7 1.9-17.1-2.9-18.9-10.7s3.8-15.7 12.4-17.8c.1 0 .2 0 .2-.1h.1c1.2-.3 2.8-.7 3.9-.9 5.1-1 9.2-.7 14-1.1 10.2-1.1 18.7-1.9 26.2-4.3 2.4-1 4.7-4.3 6.3-6.3l6.1-1.8c-6.9-47.5 4.8-94.2 29.8-131.9l-4.7-4.2c-.3-1.8-.7-6-2.9-8.4-5.8-5.4-13-9.9-21.8-15.3-4.2-2.4-8-4-12.1-7.1-.9-.7-2.1-1.7-3-2.4-.1-.1-.1-.1-.2-.2-7-5.6-8.6-15.2-3.6-21.6 2.8-3.6 7.2-5.3 11.7-5.2 3.5.1 7.1 1.4 10.2 3.8 1 .8 2.4 1.8 3.2 2.6 3.9 3.4 6.3 6.7 9.6 10.2 7.2 7.3 13.2 13.4 19.7 17.8 3.4 2 6.1 1.2 8.7.8.8.6 3.7 2.6 5.3 3.8 24.9-26.4 57.6-46 95.6-54.6 8.8-2 17.7-3.3 26.4-4.1l.3-6.2c1.9-1.9 4.1-4.6 4.8-7.6.6-7.9-.4-16.3-1.6-26.5-.7-4.8-1.8-8.7-2-13.9 0-1.1 0-2.5 0-3.8 0-.1 0-.3 0-.4 0-9 6.5-16.2 14.6-16.2s14.6 7.3 14.6 16.2c0 1.3.1 3 0 4.2-.2 5.2-1.3 9.1-2 13.9-1.2 10.2-2.3 18.7-1.7 26.5.6 3.9 2.9 5.5 4.8 7.3 0 1.1.2 4.6.3 6.5 46.5 4.1 89.7 25.4 121.4 58.7l5.6-4c1.9.1 6 .7 8.9-1 6.5-4.4 12.5-10.5 19.7-17.8 3.3-3.5 5.7-6.8 9.7-10.2.9-.8 2.3-1.8 3.2-2.6 7-5.6 16.8-5 21.8 1.3s3.4 16-3.6 21.6c-1 .8-2.3 1.9-3.2 2.6-4.2 3.1-8 4.7-12.2 7.1-8.7 5.4-16 9.9-21.8 15.3-2.7 2.9-2.5 5.7-2.8 8.3-.8.7-3.7 3.3-5.2 4.7 12.6 18.8 22.1 40.1 27.4 63.3 5.3 23.1 6.1 46.1 3.1 68.3l5.9 1.7c1.1 1.5 3.2 5.2 6.3 6.3 7.5 2.4 16 3.2 26.2 4.3 4.8.4 8.9.2 14 1.1 1.2.2 3 .7 4.2 1 8.9 2.4 14.4 10.4 12.6 18.2z"
|
||||
/>
|
||||
<path
|
||||
d="m428 401.7c-1-.2-2-.3-3-.2-1.7.1-3.3.5-4.9 1.3-6.2 3-9 10.4-6.2 16.7l-.1.1 29.6 71.4c28.5-18.2 49.8-45.3 61-76.6l-76.2-12.9z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="intro-contents box grow"
|
||||
>
|
||||
<div
|
||||
class="description flex align-center justify-space-between"
|
||||
data-testid="selected-chart-description"
|
||||
>
|
||||
|
||||
<button
|
||||
class="Button primary"
|
||||
type="button"
|
||||
>
|
||||
Install
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="DrawerItem version"
|
||||
>
|
||||
<span
|
||||
class="name"
|
||||
>
|
||||
Version
|
||||
</span>
|
||||
<span
|
||||
class="value"
|
||||
>
|
||||
<div
|
||||
class="Select theme-outlined css-b62m3t-container"
|
||||
>
|
||||
<span
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
id="react-select-chart-version-input-live-region"
|
||||
/>
|
||||
<span
|
||||
aria-atomic="false"
|
||||
aria-live="polite"
|
||||
aria-relevant="additions text"
|
||||
class="css-1f43avz-a11yText-A11yText"
|
||||
/>
|
||||
<div
|
||||
class="Select__control css-1s2u09g-control"
|
||||
>
|
||||
<div
|
||||
class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
|
||||
>
|
||||
<div
|
||||
class="Select__single-value css-qc6sy-singleValue"
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<div
|
||||
class="Select__input-container css-6j8wv5-Input"
|
||||
data-value=""
|
||||
>
|
||||
<input
|
||||
aria-autocomplete="list"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
class="Select__input"
|
||||
id="chart-version-input"
|
||||
role="combobox"
|
||||
spellcheck="false"
|
||||
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||
>
|
||||
<span
|
||||
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="css-tj5bde-Svg"
|
||||
focusable="false"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<path
|
||||
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DrawerItem"
|
||||
>
|
||||
<span
|
||||
class="name"
|
||||
>
|
||||
Home
|
||||
</span>
|
||||
<span
|
||||
class="value"
|
||||
>
|
||||
<a
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="DrawerItem maintainers"
|
||||
>
|
||||
<span
|
||||
class="name"
|
||||
>
|
||||
Maintainers
|
||||
</span>
|
||||
<span
|
||||
class="value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="chart-description"
|
||||
data-testid="helmchart-readme"
|
||||
>
|
||||
<div
|
||||
class="MarkDownViewer"
|
||||
>
|
||||
<p>
|
||||
I am a readme
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ResizingAnchor horizontal leading"
|
||||
/>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`<HelmChartDetails /> before getChartDetails resolves with getChartDetails rejects renders 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="Notifications flex column align-flex-end"
|
||||
>
|
||||
<div
|
||||
class="Animate opacity notification flex error enter"
|
||||
style="--enter-duration: 100ms; --leave-duration: 100ms;"
|
||||
>
|
||||
<div
|
||||
class="box"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="info_outline"
|
||||
>
|
||||
info_outline
|
||||
</span>
|
||||
</i>
|
||||
</div>
|
||||
<div
|
||||
class="message box grow"
|
||||
>
|
||||
Error: some error
|
||||
</div>
|
||||
<div
|
||||
class="box"
|
||||
>
|
||||
<i
|
||||
class="Icon close material interactive focusable"
|
||||
data-testid="close-notification-for-notification_20"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="close"
|
||||
>
|
||||
close
|
||||
</span>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Animate slide-right Drawer HelmChartDetails right enter"
|
||||
style="--size: 725px; --enter-duration: 100ms; --leave-duration: 100ms;"
|
||||
>
|
||||
<div
|
||||
class="drawer-wrapper flex column"
|
||||
>
|
||||
<div
|
||||
class="drawer-title flex align-center"
|
||||
>
|
||||
<div
|
||||
class="drawer-title-text flex gaps align-center"
|
||||
>
|
||||
Chart: a galaxy far far away/a name
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
id="tooltip_target_18"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="content_copy"
|
||||
>
|
||||
content_copy
|
||||
</span>
|
||||
<div />
|
||||
</i>
|
||||
</div>
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
id="tooltip_target_19"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="close"
|
||||
>
|
||||
close
|
||||
</span>
|
||||
<div />
|
||||
</i>
|
||||
</div>
|
||||
<div
|
||||
class="drawer-content flex column box grow"
|
||||
>
|
||||
<div
|
||||
class="Spinner singleColor center"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ResizingAnchor horizontal leading"
|
||||
/>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, 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";
|
||||
|
||||
const readmeOfSelectedHelmChartInjectable = getInjectable({
|
||||
id: "readme-of-selected-helm-chart",
|
||||
|
||||
instantiate: (di, chart: HelmChart) => {
|
||||
const selection = di.inject(helmChartDetailsVersionSelectionInjectable, chart);
|
||||
const callForHelmChartReadme = di.inject(callForHelmChartReadmeInjectable);
|
||||
|
||||
return asyncComputed(async () => {
|
||||
const chartVersion = selection.value.get();
|
||||
|
||||
if (!chartVersion) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return await callForHelmChartReadme(
|
||||
chartVersion.getRepository(),
|
||||
chartVersion.getName(),
|
||||
chartVersion.getVersion(),
|
||||
);
|
||||
}, "");
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, chart: HelmChart) => chart.getId(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default readmeOfSelectedHelmChartInjectable;
|
||||
@ -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 } 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;
|
||||
@ -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, 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";
|
||||
|
||||
const versionsOfSelectedHelmChartInjectable = getInjectable({
|
||||
id: "versions-of-selected-helm-chart",
|
||||
|
||||
instantiate: (di, chart: HelmChart) => {
|
||||
const callForHelmChartVersions = di.inject(callForHelmChartVersionsInjectable);
|
||||
|
||||
return asyncComputed(
|
||||
async () =>
|
||||
await callForHelmChartVersions(chart.getRepository(), chart.getName()),
|
||||
[],
|
||||
);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, chart: HelmChart) => chart.getId(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default versionsOfSelectedHelmChartInjectable;
|
||||
@ -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 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;
|
||||
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 type { IComputedValue } from "mobx";
|
||||
import { computed, observable } from "mobx";
|
||||
import versionsOfSelectedHelmChartInjectable from "../versions-of-selected-helm-chart.injectable";
|
||||
import type { HelmChart } from "../../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import type { SingleValue } from "react-select";
|
||||
|
||||
interface VersionSelectionOption {
|
||||
label: string;
|
||||
value: HelmChart;
|
||||
}
|
||||
|
||||
export interface HelmChartDetailsVersionSelection {
|
||||
value: IComputedValue<HelmChart | undefined>;
|
||||
options: IComputedValue<VersionSelectionOption[]>;
|
||||
onChange: (option: SingleValue<VersionSelectionOption>) => void;
|
||||
}
|
||||
|
||||
const helmChartDetailsVersionSelectionInjectable = getInjectable({
|
||||
id: "helm-chart-details-version-selection",
|
||||
|
||||
instantiate: (di, chart: HelmChart): HelmChartDetailsVersionSelection => {
|
||||
const versionsOfSelectedHelmChart = di.inject(
|
||||
versionsOfSelectedHelmChartInjectable,
|
||||
chart,
|
||||
);
|
||||
|
||||
const state = observable.box<HelmChart>();
|
||||
|
||||
return {
|
||||
value: computed(
|
||||
() => state.get() || versionsOfSelectedHelmChart.value.get()[0],
|
||||
),
|
||||
|
||||
options: computed(() =>
|
||||
versionsOfSelectedHelmChart.value.get().map((chartVersion) => ({
|
||||
label: chartVersion.version,
|
||||
value: chartVersion,
|
||||
})),
|
||||
),
|
||||
|
||||
onChange: (option) => {
|
||||
if (option) {
|
||||
state.set(option.value);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, chart: HelmChart) => chart.getId(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default helmChartDetailsVersionSelectionInjectable;
|
||||
@ -1,92 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import type { RenderResult } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||
import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import { noop } from "../../utils";
|
||||
import type { CreateInstallChartTab } from "../dock/install-chart/create-install-chart-tab.injectable";
|
||||
import createInstallChartTabInjectable from "../dock/install-chart/create-install-chart-tab.injectable";
|
||||
import { Notifications } from "../notifications";
|
||||
import type { DiRender } from "../test-utils/renderFor";
|
||||
import { renderFor } from "../test-utils/renderFor";
|
||||
import type { GetChartDetails } from "./get-char-details.injectable";
|
||||
import getChartDetailsInjectable from "./get-char-details.injectable";
|
||||
import { HelmChartDetails } from "./helm-chart-details";
|
||||
|
||||
describe("<HelmChartDetails />", () => {
|
||||
let di: DiContainer;
|
||||
let getChartDetails: AsyncFnMock<GetChartDetails>;
|
||||
let chart: HelmChart;
|
||||
let render: DiRender;
|
||||
let result: RenderResult;
|
||||
let createInstallChartTab: jest.MockedFunction<CreateInstallChartTab>;
|
||||
|
||||
beforeEach(() => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
getChartDetails = asyncFn<GetChartDetails>();
|
||||
createInstallChartTab = jest.fn();
|
||||
chart = HelmChart.create({
|
||||
apiVersion: "some-api-version",
|
||||
created: "a long time ago",
|
||||
name: "a name",
|
||||
repo: "a galaxy far far away",
|
||||
version: "1",
|
||||
});
|
||||
|
||||
di.override(directoryForLensLocalStorageInjectable, () => "some-directory-for-lens-local-storage");
|
||||
di.override(getChartDetailsInjectable, () => getChartDetails);
|
||||
di.override(createInstallChartTabInjectable, () => createInstallChartTab);
|
||||
render = renderFor(di);
|
||||
result = render((
|
||||
<>
|
||||
<HelmChartDetails chart={chart} hideDetails={noop} />
|
||||
<Notifications />
|
||||
</>
|
||||
));
|
||||
});
|
||||
|
||||
describe("before getChartDetails resolves", () => {
|
||||
it("renders", () => {
|
||||
expect(result.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe("when getChartDetails resolves with one version", () => {
|
||||
beforeEach(async () => {
|
||||
await getChartDetails.resolve({
|
||||
readme: "I am a readme",
|
||||
versions: [chart],
|
||||
});
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(result.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows the readme", () => {
|
||||
expect(result.queryByTestId("helmchart-readme")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("shows the selected chart", () => {
|
||||
expect(result.queryByTestId("selected-chart-description")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("with getChartDetails rejects", () => {
|
||||
beforeEach(async () => {
|
||||
await getChartDetails.reject(new Error("some error"));
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(result.baseElement).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -7,8 +7,7 @@ import "./helm-chart-details.scss";
|
||||
|
||||
import React, { Component } from "react";
|
||||
import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { computed, observable, reaction, runInAction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Drawer, DrawerItem } from "../drawer";
|
||||
import { autoBind, stopPropagation } from "../../utils";
|
||||
import { MarkdownViewer } from "../markdown-viewer";
|
||||
@ -17,19 +16,19 @@ import { Button } from "../button";
|
||||
import { Select } from "../select";
|
||||
import { Badge } from "../badge";
|
||||
import { Tooltip, withStyles } from "@material-ui/core";
|
||||
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import createInstallChartTabInjectable from "../dock/install-chart/create-install-chart-tab.injectable";
|
||||
import type { ShowCheckedErrorNotification } from "../notifications/show-checked-error.injectable";
|
||||
import type { SingleValue } from "react-select";
|
||||
import AbortController from "abort-controller";
|
||||
import showCheckedErrorNotificationInjectable from "../notifications/show-checked-error.injectable";
|
||||
import type { GetChartDetails } from "./get-char-details.injectable";
|
||||
import getChartDetailsInjectable from "./get-char-details.injectable";
|
||||
import { HelmChartIcon } from "./icon";
|
||||
import readmeOfSelectHelmChartInjectable from "./details/readme-of-selected-helm-chart.injectable";
|
||||
import versionsOfSelectedHelmChartInjectable from "./details/versions-of-selected-helm-chart.injectable";
|
||||
import type { HelmChartDetailsVersionSelection } from "./details/versions/helm-chart-details-version-selection.injectable";
|
||||
import helmChartDetailsVersionSelectionInjectable from "./details/versions/helm-chart-details-version-selection.injectable";
|
||||
import assert from "assert";
|
||||
|
||||
export interface HelmChartDetailsProps {
|
||||
chart: HelmChart;
|
||||
hideDetails(): void;
|
||||
chart: HelmChart;
|
||||
}
|
||||
|
||||
const LargeTooltip = withStyles({
|
||||
@ -40,85 +39,34 @@ const LargeTooltip = withStyles({
|
||||
|
||||
interface Dependencies {
|
||||
createInstallChartTab: (helmChart: HelmChart) => void;
|
||||
showCheckedErrorNotification: ShowCheckedErrorNotification;
|
||||
getChartDetails: GetChartDetails;
|
||||
versions: IAsyncComputed<HelmChart[]>;
|
||||
readme: IAsyncComputed<string>;
|
||||
versionSelection: HelmChartDetailsVersionSelection;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Dependencies> {
|
||||
readonly chartVersions = observable.array<HelmChart>();
|
||||
readonly selectedChart = observable.box<HelmChart | undefined>();
|
||||
readonly readme = observable.box<string | undefined>(undefined);
|
||||
readonly chartVerionOptions = computed(() => (
|
||||
this.chartVersions.map(chart => ({
|
||||
value: chart,
|
||||
label: chart.version,
|
||||
}))
|
||||
));
|
||||
|
||||
private abortController = new AbortController();
|
||||
|
||||
constructor(props: HelmChartDetailsProps & Dependencies) {
|
||||
super(props);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.abortController.abort();
|
||||
get chart() {
|
||||
return this.props.chart;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.chart, async ({ name, repo, version }) => {
|
||||
runInAction(() => {
|
||||
this.selectedChart.set(undefined);
|
||||
this.chartVersions.clear();
|
||||
this.readme.set("");
|
||||
});
|
||||
install() {
|
||||
const chart = this.props.versionSelection.value.get();
|
||||
|
||||
try {
|
||||
const { readme, versions } = await this.props.getChartDetails(repo, name, { version });
|
||||
assert(chart);
|
||||
|
||||
runInAction(() => {
|
||||
this.readme.set(readme);
|
||||
this.chartVersions.replace(versions);
|
||||
this.selectedChart.set(versions[0]);
|
||||
});
|
||||
} catch (error) {
|
||||
this.props.showCheckedErrorNotification(error, "Unknown error occured while getting chart details");
|
||||
}
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
async onVersionChange(option: SingleValue<{ value: HelmChart }>) {
|
||||
const chart = option?.value ?? this.chartVersions[0];
|
||||
|
||||
runInAction(() => {
|
||||
this.selectedChart.set(chart ?? undefined);
|
||||
this.readme.set(undefined);
|
||||
});
|
||||
|
||||
try {
|
||||
this.abortController.abort();
|
||||
this.abortController = new AbortController();
|
||||
const { chart: { name, repo }} = this.props;
|
||||
const { readme } = await this.props.getChartDetails(repo, name, { version: chart.version, reqInit: { signal: this.abortController.signal }});
|
||||
|
||||
this.readme.set(readme);
|
||||
} catch (error) {
|
||||
this.props.showCheckedErrorNotification(error, "Unknown error occured while getting chart details");
|
||||
}
|
||||
}
|
||||
|
||||
install(selectedChart: HelmChart) {
|
||||
this.props.createInstallChartTab(selectedChart);
|
||||
this.props.createInstallChartTab(chart);
|
||||
this.props.hideDetails();
|
||||
}
|
||||
|
||||
renderIntroduction(selectedChart: HelmChart) {
|
||||
const testId = selectedChart.getFullName("-");
|
||||
|
||||
return (
|
||||
<div className="introduction flex align-flex-start">
|
||||
<HelmChartIcon
|
||||
@ -131,7 +79,8 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
|
||||
<Button
|
||||
primary
|
||||
label="Install"
|
||||
onClick={() => this.install(selectedChart)}
|
||||
onClick={this.install}
|
||||
data-testid={`install-chart-for-${testId}`}
|
||||
/>
|
||||
</div>
|
||||
<DrawerItem
|
||||
@ -140,10 +89,10 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
|
||||
onClick={stopPropagation}
|
||||
>
|
||||
<Select
|
||||
id="chart-version-input"
|
||||
id={`helm-chart-version-selector-${testId}`}
|
||||
themeName="outlined"
|
||||
menuPortalTarget={null}
|
||||
options={this.chartVerionOptions.get()}
|
||||
options={this.props.versionSelection.options.get()}
|
||||
formatOptionLabel={({ value: chart }) => (
|
||||
chart.deprecated
|
||||
? (
|
||||
@ -154,8 +103,8 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
|
||||
: chart.version
|
||||
)}
|
||||
isOptionDisabled={({ value: chart }) => chart.deprecated}
|
||||
value={selectedChart}
|
||||
onChange={this.onVersionChange}
|
||||
value={this.props.versionSelection.value.get()}
|
||||
onChange={this.props.versionSelection.onChange}
|
||||
/>
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Home">
|
||||
@ -190,44 +139,42 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
|
||||
}
|
||||
|
||||
renderReadme() {
|
||||
const readme = this.readme.get();
|
||||
|
||||
if (readme === undefined) {
|
||||
return <Spinner center />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chart-description" data-testid="helmchart-readme">
|
||||
<MarkdownViewer markdown={readme} />
|
||||
<MarkdownViewer markdown={this.props.readme.value.get()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const selectedChart = this.selectedChart.get();
|
||||
const readmeIsLoading = this.props.readme.pending.get();
|
||||
const versionsAreLoading = this.props.versions.pending.get();
|
||||
|
||||
if (!selectedChart) {
|
||||
return <Spinner center />;
|
||||
if (!this.chart || versionsAreLoading) {
|
||||
return <Spinner center data-testid="spinner-for-chart-details" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="box grow">
|
||||
{this.renderIntroduction(selectedChart)}
|
||||
{this.renderReadme()}
|
||||
{this.renderIntroduction(this.chart)}
|
||||
|
||||
{readmeIsLoading ? (
|
||||
<Spinner center data-testid="spinner-for-chart-readme" />
|
||||
) : (
|
||||
this.renderReadme()
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { chart, hideDetails } = this.props;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className="HelmChartDetails"
|
||||
usePortal={true}
|
||||
open={!!chart}
|
||||
title={chart ? `Chart: ${chart.getFullName()}` : ""}
|
||||
onClose={hideDetails}
|
||||
open={!!this.chart}
|
||||
title={this.chart ? `Chart: ${this.chart.getFullName()}` : ""}
|
||||
onClose={this.props.hideDetails}
|
||||
>
|
||||
{this.renderContent()}
|
||||
</Drawer>
|
||||
@ -239,7 +186,8 @@ export const HelmChartDetails = withInjectables<Dependencies, HelmChartDetailsPr
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
createInstallChartTab: di.inject(createInstallChartTabInjectable),
|
||||
showCheckedErrorNotification: di.inject(showCheckedErrorNotificationInjectable),
|
||||
getChartDetails: di.inject(getChartDetailsInjectable),
|
||||
readme: di.inject(readmeOfSelectHelmChartInjectable, props.chart),
|
||||
versions: di.inject(versionsOfSelectedHelmChartInjectable, props.chart),
|
||||
versionSelection: di.inject(helmChartDetailsVersionSelectionInjectable, props.chart),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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;
|
||||
@ -12,12 +12,15 @@ import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.ap
|
||||
import { HelmChartDetails } from "./helm-chart-details";
|
||||
import { ItemListLayout } from "../item-object-list/list-layout";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout";
|
||||
import helmChartsRouteParametersInjectable from "./helm-charts-route-parameters.injectable";
|
||||
import type { NavigateToHelmCharts } from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
|
||||
import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
|
||||
import { HelmChartIcon } from "./icon";
|
||||
import helmChartsInjectable from "./helm-charts/helm-charts.injectable";
|
||||
import selectedHelmChartInjectable from "./helm-charts/selected-helm-chart.injectable";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -34,27 +37,15 @@ interface Dependencies {
|
||||
};
|
||||
|
||||
navigateToHelmCharts: NavigateToHelmCharts;
|
||||
|
||||
charts: IAsyncComputed<HelmChart[]>;
|
||||
selectedChart: IComputedValue<HelmChart | undefined>;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedHelmCharts extends Component<Dependencies> {
|
||||
componentDidMount() {
|
||||
helmChartStore.loadAll();
|
||||
}
|
||||
|
||||
get selectedChart() {
|
||||
const chartName = this.props.routeParameters.chartName.get();
|
||||
const repo = this.props.routeParameters.repo.get();
|
||||
|
||||
if (!chartName || !repo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return helmChartStore.getByName(chartName, repo);
|
||||
}
|
||||
|
||||
onDetails = (chart: HelmChart) => {
|
||||
if (chart === this.selectedChart) {
|
||||
if (chart === this.props.selectedChart.get()) {
|
||||
this.hideDetails();
|
||||
} else {
|
||||
this.showDetails(chart);
|
||||
@ -78,6 +69,8 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const selectedChart = this.props.selectedChart.get();
|
||||
|
||||
return (
|
||||
<SiblingsInTabLayout>
|
||||
<div data-testid="page-for-helm-charts" style={{ display: "none" }}/>
|
||||
@ -87,7 +80,7 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
|
||||
tableId="helm_charts"
|
||||
className="HelmCharts"
|
||||
store={helmChartStore}
|
||||
getItems={() => helmChartStore.items}
|
||||
getItems={() => this.props.charts.value.get()}
|
||||
isSelectable={false}
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: chart => chart.getName(),
|
||||
@ -105,6 +98,7 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
|
||||
placeholder: "Search Helm Charts...",
|
||||
},
|
||||
})}
|
||||
customizeTableRowProps={(item) => ({ testId: `helm-chart-row-for-${item.getFullName("-")}` })}
|
||||
renderTableHeader={[
|
||||
{ className: "icon", showWithColumn: columnId.name },
|
||||
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||
@ -124,12 +118,12 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
|
||||
{ title: chart.getRepository(), className: chart.getRepository().toLowerCase() },
|
||||
{ className: "menu" },
|
||||
]}
|
||||
detailsItem={this.selectedChart}
|
||||
detailsItem={selectedChart}
|
||||
onDetails={this.onDetails}
|
||||
/>
|
||||
{this.selectedChart && (
|
||||
{selectedChart && (
|
||||
<HelmChartDetails
|
||||
chart={this.selectedChart}
|
||||
chart={selectedChart}
|
||||
hideDetails={this.hideDetails}
|
||||
/>
|
||||
)}
|
||||
@ -145,6 +139,8 @@ export const HelmCharts = withInjectables<Dependencies>(
|
||||
getProps: (di) => ({
|
||||
routeParameters: di.inject(helmChartsRouteParametersInjectable),
|
||||
navigateToHelmCharts: di.inject(navigateToHelmChartsInjectable),
|
||||
charts: di.inject(helmChartsInjectable),
|
||||
selectedChart: di.inject(selectedHelmChartInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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;
|
||||
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 { asyncComputed } from "@ogre-tools/injectable-react";
|
||||
import callForHelmChartsInjectable from "./call-for-helm-charts.injectable";
|
||||
|
||||
const helmChartsInjectable = getInjectable({
|
||||
id: "helm-charts",
|
||||
|
||||
instantiate: (di) => {
|
||||
const callForHelmCharts = di.inject(callForHelmChartsInjectable);
|
||||
|
||||
return asyncComputed(async () => await callForHelmCharts(), []);
|
||||
},
|
||||
});
|
||||
|
||||
export default helmChartsInjectable;
|
||||
@ -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 { computed } from "mobx";
|
||||
import helmChartsRouteParametersInjectable from "../helm-charts-route-parameters.injectable";
|
||||
import helmChartsInjectable from "./helm-charts.injectable";
|
||||
|
||||
const selectedHelmChartInjectable = getInjectable({
|
||||
id: "selected-helm-chart",
|
||||
|
||||
instantiate: (di) => {
|
||||
const { chartName, repo } = di.inject(helmChartsRouteParametersInjectable);
|
||||
const helmCharts = di.inject(helmChartsInjectable);
|
||||
|
||||
return computed(() => {
|
||||
const dereferencedChartName = chartName.get();
|
||||
const deferencedRepository = repo.get();
|
||||
|
||||
if (!dereferencedChartName || !deferencedRepository) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return helmCharts.value
|
||||
.get()
|
||||
.find(
|
||||
(chart) =>
|
||||
chart.getName() === dereferencedChartName &&
|
||||
chart.getRepository() === deferencedRepository,
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default selectedHelmChartInjectable;
|
||||
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 { HelmReleaseCreatePayload, HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { createRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
|
||||
export type CallForCreateHelmRelease = (
|
||||
payload: HelmReleaseCreatePayload
|
||||
) => Promise<HelmReleaseUpdateDetails>;
|
||||
|
||||
const callForCreateHelmReleaseInjectable = getInjectable({
|
||||
id: "call-for-create-helm-release",
|
||||
instantiate: (): CallForCreateHelmRelease => createRelease,
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default callForCreateHelmReleaseInjectable;
|
||||
@ -6,19 +6,18 @@ import { getInjectable } from "@ogre-tools/injectable";
|
||||
|
||||
import type {
|
||||
HelmReleaseCreatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import {
|
||||
createRelease,
|
||||
} from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import releasesInjectable from "../releases.injectable";
|
||||
import callForCreateHelmReleaseInjectable from "./call-for-create-helm-release.injectable";
|
||||
|
||||
const createReleaseInjectable = getInjectable({
|
||||
id: "create-release",
|
||||
|
||||
instantiate: (di) => {
|
||||
const releases = di.inject(releasesInjectable);
|
||||
const callForCreateRelease = di.inject(callForCreateHelmReleaseInjectable);
|
||||
|
||||
return async (payload: HelmReleaseCreatePayload) => {
|
||||
const release = await createRelease(payload);
|
||||
const release = await callForCreateRelease(payload);
|
||||
|
||||
releases.invalidate();
|
||||
|
||||
|
||||
@ -85,6 +85,7 @@ class NonInjectedHelmReleaseMenu extends React.Component<HelmReleaseMenuProps &
|
||||
removeConfirmationMessage={() => (
|
||||
<p>
|
||||
Remove Helm Release
|
||||
{" "}
|
||||
<b>{release.name}</b>
|
||||
?
|
||||
</p>
|
||||
|
||||
@ -77,6 +77,7 @@ class NonInjectedHelmReleases extends Component<Dependencies> {
|
||||
<div>
|
||||
<>
|
||||
Remove
|
||||
{" "}
|
||||
<b>{releaseNames}</b>
|
||||
?
|
||||
</>
|
||||
|
||||
@ -13,8 +13,7 @@ import { Notifications } from "../notifications";
|
||||
import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
import { clipboard } from "electron";
|
||||
|
||||
// todo: make as external BrowserWindow (?)
|
||||
import { kebabCase } from "lodash/fp";
|
||||
|
||||
export interface LogsDialogProps extends DialogProps {
|
||||
title: string;
|
||||
@ -26,6 +25,7 @@ export function LogsDialog({ title, logs, ...dialogProps }: LogsDialogProps) {
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className="LogsDialog"
|
||||
data-testid={`logs-dialog-for-${kebabCase(title)}`}
|
||||
>
|
||||
<Wizard
|
||||
header={<h5>{title}</h5>}
|
||||
|
||||
@ -115,6 +115,7 @@ class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
data-testid={`dock-tab-for-${id}`}
|
||||
/>
|
||||
{this.renderMenu(id)}
|
||||
</>
|
||||
|
||||
@ -11,8 +11,8 @@ import { DockTab } from "./dock-tab";
|
||||
import type { DockTab as DockTabModel } from "./dock/store";
|
||||
import { TabKind } from "./dock/store";
|
||||
import { TerminalTab } from "./terminal/dock-tab";
|
||||
import { useResizeObserver } from "../../hooks";
|
||||
import { cssVar } from "../../utils";
|
||||
import { useResizeObserver } from "../../hooks";
|
||||
|
||||
export interface DockTabsProps {
|
||||
tabs: DockTabModel[];
|
||||
|
||||
@ -124,7 +124,10 @@ class NonInjectedDock extends React.Component<DockProps & Dependencies> {
|
||||
if (!isOpen || !selectedTab) return null;
|
||||
|
||||
return (
|
||||
<div className={`tab-content ${selectedTab.kind}`} style={{ flexBasis: height }}>
|
||||
<div
|
||||
className={`tab-content ${selectedTab.kind}`}
|
||||
style={{ flexBasis: height }}
|
||||
data-testid={`dock-tab-content-for-${selectedTab.id}`}>
|
||||
{this.renderTab(selectedTab)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -8,3 +8,7 @@
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ export interface EditorPanelProps {
|
||||
autoFocus?: boolean; // default: true
|
||||
onChange: MonacoEditorProps["onChange"];
|
||||
onError?: MonacoEditorProps["onError"];
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
@ -36,6 +37,7 @@ const NonInjectedEditorPanel = observer(({
|
||||
autoFocus = true,
|
||||
className,
|
||||
onError,
|
||||
hidden,
|
||||
}: Dependencies & EditorPanelProps) => {
|
||||
const editor = createRef<MonacoEditorRef>();
|
||||
|
||||
@ -59,7 +61,7 @@ const NonInjectedEditorPanel = observer(({
|
||||
autoFocus={autoFocus}
|
||||
id={tabId}
|
||||
value={value}
|
||||
className={cssNames(styles.EditorPanel, className)}
|
||||
className={cssNames(styles.EditorPanel, className, { hidden })}
|
||||
onChange={onChange}
|
||||
onError={onError}
|
||||
ref={editor}
|
||||
|
||||
@ -14,9 +14,12 @@ import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
import { Spinner } from "../spinner";
|
||||
import type { DockStore, TabId } from "./dock/store";
|
||||
import { Notifications } from "../notifications";
|
||||
import type { ShowNotification } from "../notifications";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import dockStoreInjectable from "./dock/store.injectable";
|
||||
import type { ShowCheckedErrorNotification } from "../notifications/show-checked-error.injectable";
|
||||
import showSuccessNotificationInjectable from "../notifications/show-success-notification.injectable";
|
||||
import showCheckedErrorNotificationInjectable from "../notifications/show-checked-error.injectable";
|
||||
|
||||
export interface InfoPanelProps extends OptionalProps {
|
||||
tabId: TabId;
|
||||
@ -35,10 +38,15 @@ export interface OptionalProps {
|
||||
showInlineInfo?: boolean;
|
||||
showNotifications?: boolean;
|
||||
showStatusPanel?: boolean;
|
||||
submitTestId?: string;
|
||||
cancelTestId?: string;
|
||||
submittingTestId?: string;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
dockStore: DockStore;
|
||||
showSuccessNotification: ShowNotification;
|
||||
showCheckedErrorNotification: ShowCheckedErrorNotification;
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -82,11 +90,11 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
|
||||
const result = await this.props.submit?.();
|
||||
|
||||
if (showNotifications && result) {
|
||||
Notifications.ok(result);
|
||||
this.props.showSuccessNotification(result);
|
||||
}
|
||||
} catch (error) {
|
||||
if (showNotifications) {
|
||||
Notifications.checkedError(error, "Unknown error while submitting");
|
||||
this.props.showCheckedErrorNotification(error, "Unknown error while submitting");
|
||||
}
|
||||
} finally {
|
||||
this.waiting = false;
|
||||
@ -128,7 +136,7 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
|
||||
<div className="flex gaps align-center">
|
||||
{waiting ? (
|
||||
<>
|
||||
<Spinner />
|
||||
<Spinner data-testid={this.props.submittingTestId} />
|
||||
{" "}
|
||||
{submittingMessage}
|
||||
</>
|
||||
@ -140,7 +148,8 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
|
||||
<Button
|
||||
plain
|
||||
label="Cancel"
|
||||
onClick={close}
|
||||
onClick={close}
|
||||
data-testid={this.props.cancelTestId}
|
||||
/>
|
||||
<Button
|
||||
active
|
||||
@ -149,6 +158,7 @@ class NonInjectedInfoPanel extends Component<InfoPanelProps & Dependencies> {
|
||||
label={submitLabel}
|
||||
onClick={submit}
|
||||
disabled={isDisabled}
|
||||
data-testid={this.props.submitTestId}
|
||||
/>
|
||||
{showSubmitClose && (
|
||||
<Button
|
||||
@ -172,6 +182,8 @@ export const InfoPanel = withInjectables<Dependencies, InfoPanelProps>(
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
showSuccessNotification: di.inject(showSuccessNotificationInjectable),
|
||||
showCheckedErrorNotification: di.inject(showCheckedErrorNotificationInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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,52 +5,46 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import installChartTabStoreInjectable from "./store.injectable";
|
||||
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import type {
|
||||
DockTab,
|
||||
DockTabCreate,
|
||||
DockTabCreateSpecific } from "../dock/store";
|
||||
import type { DockTab, DockTabCreateSpecific } from "../dock/store";
|
||||
import { TabKind } from "../dock/store";
|
||||
import type { InstallChartTabStore } from "./store";
|
||||
import createDockTabInjectable from "../dock/create-dock-tab.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
createDockTab: (rawTab: DockTabCreate, addNumber: boolean) => DockTab;
|
||||
installChartStore: InstallChartTabStore;
|
||||
}
|
||||
import getRandomInstallChartTabIdInjectable from "./get-random-install-chart-tab-id.injectable";
|
||||
|
||||
export type CreateInstallChartTab = (chart: HelmChart, tabParams?: DockTabCreateSpecific) => DockTab;
|
||||
|
||||
const createInstallChartTab = ({ createDockTab, installChartStore }: Dependencies) => (chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => {
|
||||
const { name, repo, version } = chart;
|
||||
|
||||
const tab = createDockTab(
|
||||
{
|
||||
title: `Helm Install: ${repo}/${name}`,
|
||||
...tabParams,
|
||||
kind: TabKind.INSTALL_CHART,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
installChartStore.setData(tab.id, {
|
||||
name,
|
||||
repo,
|
||||
version,
|
||||
namespace: "default",
|
||||
releaseName: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
return tab;
|
||||
};
|
||||
|
||||
const createInstallChartTabInjectable = getInjectable({
|
||||
id: "create-install-chart-tab",
|
||||
|
||||
instantiate: (di) => createInstallChartTab({
|
||||
installChartStore: di.inject(installChartTabStoreInjectable),
|
||||
createDockTab: di.inject(createDockTabInjectable),
|
||||
}),
|
||||
instantiate: (di) => {
|
||||
const installChartStore = di.inject(installChartTabStoreInjectable);
|
||||
const createDockTab = di.inject(createDockTabInjectable);
|
||||
const getRandomId = di.inject(getRandomInstallChartTabIdInjectable);
|
||||
|
||||
return (chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => {
|
||||
const { name, repo, version } = chart;
|
||||
|
||||
const tab = createDockTab(
|
||||
{
|
||||
id: getRandomId(),
|
||||
title: `Helm Install: ${repo}/${name}`,
|
||||
...tabParams,
|
||||
kind: TabKind.INSTALL_CHART,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
installChartStore.setData(tab.id, {
|
||||
name,
|
||||
repo,
|
||||
version,
|
||||
namespace: "default",
|
||||
releaseName: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
return tab;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default createInstallChartTabInjectable;
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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 getRandomIdInjectable from "../../../../common/utils/get-random-id.injectable";
|
||||
|
||||
const getRandomInstallChartTabIdInjectable = getInjectable({
|
||||
id: "get-random-install-chart-tab-id",
|
||||
instantiate: (di) => di.inject(getRandomIdInjectable),
|
||||
});
|
||||
|
||||
export default getRandomInstallChartTabIdInjectable;
|
||||
@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 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 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 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 { 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";
|
||||
|
||||
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 dockStore = di.inject(dockStoreInjectable);
|
||||
const navigateToHelmReleases = di.inject(navigateToHelmReleasesInjectable);
|
||||
const closeTab = () => dockStore.closeTab(tabId);
|
||||
|
||||
const waitForChart = async () => {
|
||||
await waitUntilDefined(() => store.getData(tabId));
|
||||
};
|
||||
|
||||
const model = new InstallChartModel({
|
||||
tabId,
|
||||
waitForChart,
|
||||
callForCreateHelmRelease,
|
||||
closeTab,
|
||||
navigateToHelmReleases,
|
||||
callForHelmChartValues,
|
||||
callForHelmChartVersions,
|
||||
store,
|
||||
});
|
||||
|
||||
await model.load();
|
||||
|
||||
return model;
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.keyedSingleton({
|
||||
getInstanceKey: (di, tabId: string) => tabId,
|
||||
}),
|
||||
});
|
||||
|
||||
export default installChartModelInjectable;
|
||||
|
||||
interface Dependencies {
|
||||
tabId: string;
|
||||
closeTab: () => void;
|
||||
navigateToHelmReleases: NavigateToHelmReleases;
|
||||
waitForChart: () => Promise<void>;
|
||||
callForCreateHelmRelease: CallForCreateHelmRelease;
|
||||
callForHelmChartValues: CallForHelmChartValues;
|
||||
callForHelmChartVersions: CallForHelmChartVersions;
|
||||
store: InstallChartTabStore;
|
||||
}
|
||||
|
||||
export class InstallChartModel {
|
||||
readonly namespace = {
|
||||
value: computed(() => this.chart?.namespace || "default"),
|
||||
|
||||
onChange: action(
|
||||
(option: SingleValue<{ label: string; value: string }>) => {
|
||||
if (option) {
|
||||
const namespace = option.value;
|
||||
|
||||
this.save({ namespace });
|
||||
}
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
readonly customName = {
|
||||
value: computed(() => this.chart?.releaseName || ""),
|
||||
|
||||
onChange: action((customName: string) => {
|
||||
this.save({ releaseName: customName });
|
||||
}),
|
||||
};
|
||||
|
||||
private readonly versions = observable.array<HelmChart>([]);
|
||||
readonly installed = observable.box<HelmReleaseUpdateDetails | undefined>();
|
||||
|
||||
private save = (data: Partial<IChartInstallData>) => {
|
||||
assert(this.chart);
|
||||
|
||||
const chart = { ...this.chart, ...data };
|
||||
|
||||
this.dependencies.store.setData(this.dependencies.tabId, chart);
|
||||
};
|
||||
|
||||
readonly version = {
|
||||
value: computed(() => this.chart?.version),
|
||||
|
||||
onChange: async (version: string | undefined) => {
|
||||
assert(this.chart);
|
||||
|
||||
if (!version) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.save({ version });
|
||||
|
||||
runInAction(() => {
|
||||
this.configuration.isLoading.set(true);
|
||||
});
|
||||
|
||||
const configuration = await this.dependencies.callForHelmChartValues(
|
||||
this.chart.repo,
|
||||
this.chart.name,
|
||||
version,
|
||||
);
|
||||
|
||||
runInAction(() => {
|
||||
this.configuration.onChange(configuration);
|
||||
this.configuration.isLoading.set(false);
|
||||
});
|
||||
},
|
||||
|
||||
options: computed(() =>
|
||||
this.versions.map((chart) => ({
|
||||
label: chart.version,
|
||||
value: chart.version,
|
||||
})),
|
||||
),
|
||||
};
|
||||
|
||||
readonly configuration = {
|
||||
value: computed(() => this.chart?.values || ""),
|
||||
isLoading: observable.box(false),
|
||||
|
||||
onChange: action((configuration: string) => {
|
||||
this.errorInConfiguration.value.set(undefined);
|
||||
|
||||
this.save({ values: configuration });
|
||||
}),
|
||||
};
|
||||
|
||||
readonly errorInConfiguration = {
|
||||
value: observable.box<string | undefined>(),
|
||||
|
||||
onChange: action((error: unknown) => {
|
||||
this.errorInConfiguration.value.set(error as string);
|
||||
}),
|
||||
};
|
||||
|
||||
readonly executionOutput = {
|
||||
isShown: observable.box(false),
|
||||
|
||||
show: action(() => {
|
||||
this.executionOutput.isShown.set(true);
|
||||
}),
|
||||
|
||||
close: action(() => {
|
||||
this.executionOutput.isShown.set(false);
|
||||
}),
|
||||
};
|
||||
|
||||
constructor(private readonly dependencies: Dependencies) {}
|
||||
|
||||
@computed
|
||||
private get chart() {
|
||||
const chart = this.dependencies.store.getData(this.dependencies.tabId);
|
||||
|
||||
assert(chart);
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
load = async () => {
|
||||
await this.dependencies.waitForChart();
|
||||
|
||||
const [defaultConfiguration, versions] = await Promise.all([
|
||||
this.dependencies.callForHelmChartValues(
|
||||
this.chart.repo,
|
||||
this.chart.name,
|
||||
this.chart.version,
|
||||
),
|
||||
|
||||
this.dependencies.callForHelmChartVersions(
|
||||
this.chart.repo,
|
||||
this.chart.name,
|
||||
),
|
||||
]);
|
||||
|
||||
runInAction(() => {
|
||||
// TODO: Make "default" not hard-coded
|
||||
const namespace = this.chart.namespace || "default";
|
||||
|
||||
this.versions.replace(versions);
|
||||
|
||||
this.save({
|
||||
version: this.chart.version,
|
||||
namespace,
|
||||
values: this.chart.values || defaultConfiguration,
|
||||
releaseName: this.chart.releaseName,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@computed
|
||||
get isValid() {
|
||||
return !this.configuration.isLoading.get();
|
||||
}
|
||||
|
||||
get chartName() {
|
||||
return `${this.repository}/${this.name}`;
|
||||
}
|
||||
|
||||
private get name() {
|
||||
assert(this.chart);
|
||||
|
||||
return this.chart.name;
|
||||
}
|
||||
|
||||
private get repository() {
|
||||
assert(this.chart);
|
||||
|
||||
return this.chart.repo;
|
||||
}
|
||||
|
||||
install = async () => {
|
||||
const installed = await this.dependencies.callForCreateHelmRelease({
|
||||
name: this.customName.value.get() || undefined,
|
||||
chart: this.name,
|
||||
repo: this.repository,
|
||||
namespace: this.namespace.value.get() || "",
|
||||
version: this.version.value.get() || "",
|
||||
values: this.configuration.value.get() || "",
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
this.installed.set(installed);
|
||||
});
|
||||
|
||||
return (
|
||||
<p>
|
||||
{"Chart Release "}
|
||||
<b>{installed.release.name}</b>
|
||||
{" successfully created."}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
navigateToInstalledRelease = () => {
|
||||
const installed = this.installed.get();
|
||||
|
||||
assert(installed);
|
||||
|
||||
const release = installed.release;
|
||||
|
||||
this.dependencies.navigateToHelmReleases({
|
||||
name: release.name,
|
||||
namespace: release.namespace,
|
||||
});
|
||||
|
||||
this.dependencies.closeTab();
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,13 +3,10 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { action, makeObservable } from "mobx";
|
||||
import type { TabId } from "../dock/store";
|
||||
import { makeObservable } from "mobx";
|
||||
import type { DockTabStoreDependencies } from "../dock-tab-store/dock-tab.store";
|
||||
import { DockTabStore } from "../dock-tab-store/dock-tab.store";
|
||||
import { getChartDetails, getChartValues } from "../../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import type { HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { waitUntilDefined } from "../../../../common/utils/wait";
|
||||
|
||||
export interface IChartInstallData {
|
||||
name: string;
|
||||
@ -40,42 +37,4 @@ export class InstallChartTabStore extends DockTabStore<IChartInstallData> {
|
||||
get details() {
|
||||
return this.dependencies.detailsStore;
|
||||
}
|
||||
|
||||
@action
|
||||
async loadData(tabId: string) {
|
||||
const promises = [];
|
||||
const data = await waitUntilDefined(() => this.getData(tabId));
|
||||
|
||||
if (!this.getData(tabId)?.values) {
|
||||
promises.push(this.loadValues(tabId));
|
||||
}
|
||||
|
||||
if (!this.versions.getData(tabId)) {
|
||||
promises.push(this.loadVersions(tabId, data));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
@action
|
||||
private async loadVersions(tabId: TabId, { repo, name, version }: IChartInstallData) {
|
||||
this.versions.clearData(tabId); // reset
|
||||
const charts = await getChartDetails(repo, name, { version });
|
||||
const versions = charts.versions.map(chartVersion => chartVersion.version);
|
||||
|
||||
this.versions.setData(tabId, versions);
|
||||
}
|
||||
|
||||
@action
|
||||
async loadValues(tabId: TabId, attempt = 0): Promise<void> {
|
||||
const data = await waitUntilDefined(() => this.getData(tabId));
|
||||
const { repo, name, version } = data;
|
||||
const values = await getChartValues(repo, name, version);
|
||||
|
||||
if (values) {
|
||||
this.setData(tabId, { ...data, values });
|
||||
} else if (attempt < 4) {
|
||||
return this.loadValues(tabId, attempt + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,154 +5,44 @@
|
||||
|
||||
import "./install-chart.scss";
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { action, makeObservable, observable } from "mobx";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { DockStore, DockTab } from "../dock/store";
|
||||
import type { DockTab } from "../dock/store";
|
||||
import { InfoPanel } from "../info-panel";
|
||||
import { Badge } from "../../badge";
|
||||
import { NamespaceSelect } from "../../+namespaces/namespace-select";
|
||||
import { prevDefault } from "../../../utils";
|
||||
import type { IChartInstallData, InstallChartTabStore } from "./store";
|
||||
import { Spinner } from "../../spinner";
|
||||
import { Icon } from "../../icon";
|
||||
import { Button } from "../../button";
|
||||
import { LogsDialog } from "../../dialog/logs-dialog";
|
||||
import type { SelectOption } from "../../select";
|
||||
import { Select } from "../../select";
|
||||
import { Input } from "../../input";
|
||||
import { EditorPanel } from "../editor-panel";
|
||||
import type { HelmReleaseCreatePayload, HelmReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import installChartTabStoreInjectable from "./store.injectable";
|
||||
import dockStoreInjectable from "../dock/store.injectable";
|
||||
import createReleaseInjectable from "../../+helm-releases/create-release/create-release.injectable";
|
||||
import { Notifications } from "../../notifications";
|
||||
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 type { SingleValue } from "react-select";
|
||||
import type { InstallChartModel } from "./install-chart-model.injectable";
|
||||
import installChartModelInjectable from "./install-chart-model.injectable";
|
||||
import { Spinner } from "../../spinner";
|
||||
|
||||
export interface InstallCharProps {
|
||||
tab: DockTab;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
createRelease: (payload: HelmReleaseCreatePayload) => Promise<HelmReleaseUpdateDetails>;
|
||||
installChartStore: InstallChartTabStore;
|
||||
dockStore: DockStore;
|
||||
navigateToHelmReleases: NavigateToHelmReleases;
|
||||
model: InstallChartModel;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies> {
|
||||
@observable error = "";
|
||||
@observable showNotes = false;
|
||||
const NonInjectedInstallChart = observer(
|
||||
({ model: model, tab: { id: tabId }}: InstallCharProps & Dependencies) => {
|
||||
const installed = model.installed.get();
|
||||
|
||||
constructor(props: InstallCharProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.props.installChartStore.loadData(this.tabId)
|
||||
.catch(err => Notifications.error(String(err)));
|
||||
}
|
||||
|
||||
get chartData() {
|
||||
return this.props.installChartStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
return this.props.tab.id;
|
||||
}
|
||||
|
||||
get versions() {
|
||||
return this.props.installChartStore.versions.getData(this.tabId);
|
||||
}
|
||||
|
||||
get releaseDetails() {
|
||||
return this.props.installChartStore.details.getData(this.tabId);
|
||||
}
|
||||
|
||||
viewRelease = ({ release }: HelmReleaseUpdateDetails) => {
|
||||
this.props.navigateToHelmReleases({
|
||||
name: release.name,
|
||||
namespace: release.namespace,
|
||||
});
|
||||
this.props.dockStore.closeTab(this.tabId);
|
||||
};
|
||||
|
||||
save(data: Partial<IChartInstallData>) {
|
||||
assert(this.chartData, "Cannot update data before data exists");
|
||||
|
||||
this.props.installChartStore.setData(this.tabId, { ...this.chartData, ...data });
|
||||
}
|
||||
|
||||
onVersionChange = (option: SingleValue<SelectOption<string>>) => {
|
||||
if (option) {
|
||||
this.save({ ...option, values: "" });
|
||||
this.props.installChartStore.loadValues(this.tabId);
|
||||
}
|
||||
};
|
||||
|
||||
onChange = action((values: string) => {
|
||||
this.error = "";
|
||||
this.save({ values });
|
||||
});
|
||||
|
||||
onError = action((error: Error | string) => {
|
||||
this.error = error.toString();
|
||||
});
|
||||
|
||||
onNamespaceChange = (option: SingleValue<SelectOption<string>>) => {
|
||||
if (option) {
|
||||
this.save({ namespace: option.value });
|
||||
}
|
||||
};
|
||||
|
||||
onReleaseNameChange = (name: string) => {
|
||||
this.save({ releaseName: name });
|
||||
};
|
||||
|
||||
install = async ({ repo, name, version, namespace, values = "", releaseName }: IChartInstallData) => {
|
||||
const details = await this.props.createRelease({
|
||||
name: releaseName || undefined,
|
||||
chart: name,
|
||||
repo,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
namespace: namespace!,
|
||||
version,
|
||||
values,
|
||||
});
|
||||
|
||||
this.props.installChartStore.details.setData(this.tabId, details);
|
||||
|
||||
return (
|
||||
<p>
|
||||
{"Chart Release "}
|
||||
<b>{details.release.name}</b>
|
||||
{" successfully created."}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tabId, chartData, versions, install, releaseDetails } = this;
|
||||
|
||||
if (chartData?.values === undefined || !versions) {
|
||||
return <Spinner center />;
|
||||
}
|
||||
|
||||
if (releaseDetails) {
|
||||
if (installed) {
|
||||
return (
|
||||
<div className="InstallChartDone flex column gaps align-center justify-center">
|
||||
<p>
|
||||
<Icon
|
||||
material="check"
|
||||
big
|
||||
sticker
|
||||
/>
|
||||
sticker />
|
||||
</p>
|
||||
<p>Installation complete!</p>
|
||||
<div className="flex gaps align-center">
|
||||
@ -160,30 +50,34 @@ class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies>
|
||||
autoFocus
|
||||
primary
|
||||
label="View Helm Release"
|
||||
onClick={prevDefault(() => this.viewRelease(releaseDetails))}
|
||||
onClick={prevDefault(model.navigateToInstalledRelease)}
|
||||
data-testid={`show-release-${installed.release.name}-for-${tabId}`}
|
||||
/>
|
||||
<Button
|
||||
plain
|
||||
active
|
||||
label="Show Notes"
|
||||
onClick={() => this.showNotes = true}
|
||||
onClick={model.executionOutput.show}
|
||||
data-testid={`show-execution-output-for-${installed.release.name}-in-${tabId}`}
|
||||
/>
|
||||
</div>
|
||||
<LogsDialog
|
||||
title="Helm Chart Install"
|
||||
isOpen={this.showNotes}
|
||||
close={() => this.showNotes = false}
|
||||
logs={releaseDetails.log}
|
||||
isOpen={model.executionOutput.isShown.get()}
|
||||
close={model.executionOutput.close}
|
||||
logs={installed.log}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { repo, name, version, namespace, releaseName } = chartData;
|
||||
const versionOptions = versions.map(version => ({
|
||||
value: version,
|
||||
label: version,
|
||||
}));
|
||||
const {
|
||||
configuration,
|
||||
version,
|
||||
namespace,
|
||||
customName,
|
||||
errorInConfiguration,
|
||||
} = model;
|
||||
|
||||
return (
|
||||
<div className="InstallChart flex column">
|
||||
@ -192,60 +86,77 @@ class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies>
|
||||
controls={(
|
||||
<div className="install-controls flex gaps align-center">
|
||||
<span>Chart</span>
|
||||
<Badge label={`${repo}/${name}`} title="Repo/Name" />
|
||||
<Badge label={model.chartName} title="Repo/Name" />
|
||||
<span>Version</span>
|
||||
<Select
|
||||
className="chart-version"
|
||||
value={version}
|
||||
options={versionOptions}
|
||||
onChange={this.onVersionChange}
|
||||
value={version.value.get()}
|
||||
options={version.options.get()}
|
||||
onChange={(changed) => version.onChange(changed?.value)}
|
||||
menuPlacement="top"
|
||||
themeName="outlined"
|
||||
id={`install-chart-version-select-for-${tabId}`}
|
||||
/>
|
||||
<span>Namespace</span>
|
||||
<NamespaceSelect
|
||||
showIcons={false}
|
||||
menuPlacement="top"
|
||||
themeName="outlined"
|
||||
value={namespace}
|
||||
onChange={this.onNamespaceChange}
|
||||
value={namespace.value.get()}
|
||||
onChange={namespace.onChange}
|
||||
id={`install-chart-namespace-select-for-${tabId}`}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Name (optional)"
|
||||
title="Release name"
|
||||
maxLength={50}
|
||||
value={releaseName}
|
||||
onChange={this.onReleaseNameChange}
|
||||
value={customName.value.get()}
|
||||
onChange={customName.onChange}
|
||||
data-testid={`install-chart-custom-name-input-for-${tabId}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
error={this.error}
|
||||
submit={() => install(chartData)}
|
||||
disableSubmit={!chartData.namespace}
|
||||
error={errorInConfiguration.value.get()}
|
||||
submit={model.install}
|
||||
disableSubmit={!model.isValid} // !namespace
|
||||
submitLabel="Install"
|
||||
submittingMessage="Installing..."
|
||||
showSubmitClose={false}
|
||||
cancelTestId={`cancel-install-chart-from-tab-for-${tabId}`}
|
||||
submitTestId={`install-chart-from-tab-for-${tabId}`}
|
||||
submittingTestId={`installing-chart-from-tab-${tabId}`}
|
||||
/>
|
||||
|
||||
{configuration.isLoading.get() && (
|
||||
<Spinner center data-testid="install-chart-configuration-spinner" />
|
||||
)}
|
||||
|
||||
<EditorPanel
|
||||
tabId={tabId}
|
||||
value={chartData.values}
|
||||
onChange={this.onChange}
|
||||
onError={this.onError}
|
||||
value={configuration.value.get()}
|
||||
onChange={configuration.onChange}
|
||||
onError={errorInConfiguration.onChange}
|
||||
hidden={configuration.isLoading.get()}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const InstallChart = withInjectables<Dependencies, InstallCharProps>(
|
||||
NonInjectedInstallChart,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
createRelease: di.inject(createReleaseInjectable),
|
||||
installChartStore: di.inject(installChartTabStoreInjectable),
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable),
|
||||
getPlaceholder: () => (
|
||||
<Spinner
|
||||
center
|
||||
data-testid="install-chart-tab-spinner"
|
||||
/>
|
||||
),
|
||||
|
||||
getProps: async (di, props) => ({
|
||||
model: await di.inject(installChartModelInjectable, props.tab.id),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { editor } from "monaco-editor";
|
||||
import React from "react";
|
||||
import type { MonacoEditorProps, MonacoEditorRef } from "../monaco-editor";
|
||||
import { monacoValidators } from "../monaco-validators";
|
||||
|
||||
class FakeMonacoEditor extends React.Component<MonacoEditorProps> {
|
||||
render() {
|
||||
const { id, value, onChange, onError, language = "yaml" } = this.props;
|
||||
|
||||
return (
|
||||
<input
|
||||
data-testid={`monaco-editor-for-${id}`}
|
||||
|
||||
onChange={(event) => {
|
||||
const newValue = event.target.value;
|
||||
|
||||
onChange?.(
|
||||
newValue,
|
||||
{} as editor.IModelContentChangedEvent,
|
||||
);
|
||||
|
||||
const validator = monacoValidators[language];
|
||||
|
||||
try {
|
||||
validator(newValue);
|
||||
} catch(e) {
|
||||
onError?.(e);
|
||||
}
|
||||
}}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const MonacoEditor = React.forwardRef<
|
||||
MonacoEditorRef,
|
||||
MonacoEditorProps
|
||||
>((props, ref) => <FakeMonacoEditor innerRef={ref} {...props} />);
|
||||
@ -12,10 +12,11 @@ import type { MonacoTheme } from "./monaco-themes";
|
||||
import { type MonacoValidator, monacoValidators } from "./monaco-validators";
|
||||
import { debounce, merge } from "lodash";
|
||||
import { autoBind, cssNames, disposer } from "../../utils";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import type { UserStore } from "../../../common/user-store";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
|
||||
|
||||
export type MonacoEditorId = string;
|
||||
|
||||
@ -39,6 +40,7 @@ export interface MonacoEditorProps {
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
userStore: UserStore;
|
||||
}
|
||||
|
||||
export function createMonacoUri(id: MonacoEditorId): Uri {
|
||||
@ -99,7 +101,7 @@ class NonInjectedMonacoEditor extends React.Component<MonacoEditorProps & Depend
|
||||
|
||||
@computed get options(): editor.IStandaloneEditorConstructionOptions {
|
||||
return merge({},
|
||||
UserStore.getInstance().editorConfiguration,
|
||||
this.props.userStore.editorConfiguration,
|
||||
this.props.options,
|
||||
);
|
||||
}
|
||||
@ -305,6 +307,7 @@ export const MonacoEditor = withInjectables<Dependencies, MonacoEditorProps, Mon
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
userStore: di.inject(userStoreInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -19,15 +19,19 @@ export interface TableRowProps<Item> extends React.DOMAttributes<HTMLDivElement>
|
||||
sortItem?: Item; // data for sorting callback in <Table sortable={}/>
|
||||
searchItem?: Item; // data for searching filters in <Table searchable={}/>
|
||||
disabled?: boolean;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
export class TableRow<Item> extends React.Component<TableRowProps<Item>> {
|
||||
render() {
|
||||
const { className, nowrap, selected, disabled, children, sortItem, searchItem, ...rowProps } = this.props;
|
||||
const { className, nowrap, selected, disabled, children, sortItem, searchItem, testId, ...rowProps } = this.props;
|
||||
const classNames = cssNames("TableRow", className, { selected, nowrap, disabled });
|
||||
|
||||
return (
|
||||
<div className={classNames} {...rowProps}>
|
||||
<div
|
||||
className={classNames}
|
||||
data-testid={testId}
|
||||
{...rowProps}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -12,7 +12,7 @@ import { Router } from "react-router";
|
||||
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
|
||||
import allowedResourcesInjectable from "../../cluster-frame-context/allowed-resources.injectable";
|
||||
import type { RenderResult } from "@testing-library/react";
|
||||
import { getByText, fireEvent } from "@testing-library/react";
|
||||
import { queryByText, fireEvent } from "@testing-library/react";
|
||||
import type { KubeResource } from "../../../common/rbac";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
|
||||
@ -26,6 +26,7 @@ import type { MenuItemOpts } from "../../../main/menu/application-menu-items.inj
|
||||
import applicationMenuItemsInjectable from "../../../main/menu/application-menu-items.injectable";
|
||||
import type { MenuItemConstructorOptions, MenuItem } from "electron";
|
||||
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
|
||||
import type { NavigateToHelmCharts } from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
|
||||
import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
|
||||
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
|
||||
import { ClusterFrameContext } from "../../cluster-frame-context/cluster-frame-context";
|
||||
@ -110,12 +111,13 @@ export interface ApplicationBuilder {
|
||||
};
|
||||
|
||||
helmCharts: {
|
||||
navigate: () => void;
|
||||
navigate: NavigateToHelmCharts;
|
||||
};
|
||||
|
||||
select: {
|
||||
openMenu: (id: string) => void;
|
||||
openMenu: (id: string) => ({ selectOption: (labelText: string) => void });
|
||||
selectOption: (menuId: string, labelText: string) => void;
|
||||
getValue: (menuId: string) => string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -244,6 +246,20 @@ export const getApplicationBuilder = () => {
|
||||
const disableRendererExtension = disableExtensionsFor(rendererExtensionsState, rendererDi);
|
||||
const disableMainExtension = disableExtensionsFor(mainExtensionsState, mainDi);
|
||||
|
||||
const selectOptionFor = (menuId: string) => (labelText: string) => {
|
||||
const menuOptions = rendered.baseElement.querySelector<HTMLElement>(
|
||||
`.${menuId}-options`,
|
||||
);
|
||||
|
||||
assert(menuOptions, `Could not find select options for menu with ID "${menuId}"`);
|
||||
|
||||
const option = queryByText(menuOptions, labelText);
|
||||
|
||||
assert(option, `Could not find select option with label "${labelText}" for menu with ID "${menuId}"`);
|
||||
|
||||
userEvent.click(option);
|
||||
};
|
||||
|
||||
const builder: ApplicationBuilder = {
|
||||
dis,
|
||||
|
||||
@ -364,10 +380,10 @@ export const getApplicationBuilder = () => {
|
||||
},
|
||||
|
||||
helmCharts: {
|
||||
navigate: () => {
|
||||
navigate: (parameters) => {
|
||||
const navigateToHelmCharts = rendererDi.inject(navigateToHelmChartsInjectable);
|
||||
|
||||
navigateToHelmCharts();
|
||||
navigateToHelmCharts(parameters);
|
||||
},
|
||||
},
|
||||
|
||||
@ -391,6 +407,7 @@ export const getApplicationBuilder = () => {
|
||||
const namespaceStoreStub = {
|
||||
contextNamespaces: [],
|
||||
items: [],
|
||||
selectNamespaces: () => {},
|
||||
} as unknown as NamespaceStore;
|
||||
|
||||
const clusterFrameContextFake = new ClusterFrameContext(
|
||||
@ -500,25 +517,33 @@ export const getApplicationBuilder = () => {
|
||||
|
||||
select: {
|
||||
openMenu: (menuId) => {
|
||||
const selector = rendered.container.querySelector<HTMLElement>(
|
||||
const select = rendered.baseElement.querySelector<HTMLElement>(
|
||||
`#${menuId}`,
|
||||
);
|
||||
|
||||
assert(selector);
|
||||
assert(select, `Could not find select with ID "${menuId}"`);
|
||||
|
||||
openMenu(selector);
|
||||
openMenu(select);
|
||||
|
||||
return {
|
||||
selectOption: selectOptionFor(menuId),
|
||||
};
|
||||
},
|
||||
|
||||
selectOption: (menuId, labelText) => {
|
||||
const menuOptions = rendered.baseElement.querySelector<HTMLElement>(
|
||||
`.${menuId}-options`,
|
||||
selectOption: (menuId, labelText) => selectOptionFor(menuId)(labelText),
|
||||
|
||||
getValue: (menuId) => {
|
||||
const select = rendered.baseElement.querySelector<HTMLInputElement>(
|
||||
`#${menuId}`,
|
||||
);
|
||||
|
||||
assert(menuOptions);
|
||||
assert(select, `Could not find select with ID "${menuId}"`);
|
||||
|
||||
const option = getByText(menuOptions, labelText);
|
||||
const controlElement = select.closest(".Select__control");
|
||||
|
||||
userEvent.click(option);
|
||||
assert(controlElement, `Could not find select value for menu with ID "${menuId}"`);
|
||||
|
||||
return controlElement.textContent || "";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -372,6 +372,7 @@ exports[`<ClusterFrame /> given cluster with list nodes and namespaces permissio
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -852,6 +853,7 @@ exports[`<ClusterFrame /> given cluster with list nodes and namespaces permissio
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
@ -1391,6 +1393,7 @@ exports[`<ClusterFrame /> given cluster without list nodes, but with namespaces
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center DockTab TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
|
||||
@ -72,6 +72,7 @@ import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||
import forceUpdateModalRootFrameComponentInjectable from "./application-update/force-update-modal/force-update-modal-root-frame-component.injectable";
|
||||
import legacyOnChannelListenInjectable from "./ipc/legacy-channel-listen.injectable";
|
||||
import getEntitySettingCommandsInjectable from "./components/command-palette/registered-commands/get-entity-setting-commands.injectable";
|
||||
import storageSaveDelayInjectable from "./utils/create-storage/storage-save-delay.injectable";
|
||||
|
||||
export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) => {
|
||||
const {
|
||||
@ -111,6 +112,9 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
|
||||
|
||||
di.override(historyInjectable, () => createMemoryHistory());
|
||||
di.override(legacyOnChannelListenInjectable, () => () => noop);
|
||||
|
||||
di.override(storageSaveDelayInjectable, () => 0);
|
||||
|
||||
di.override(requestAnimationFrameInjectable, () => (callback) => callback());
|
||||
di.override(lensResourcesDirInjectable, () => "/irrelevant");
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import { observable } from "mobx";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable";
|
||||
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
|
||||
import storageSaveDelayInjectable from "./storage-save-delay.injectable";
|
||||
|
||||
const createStorageInjectable = getInjectable({
|
||||
id: "create-storage",
|
||||
@ -27,6 +28,7 @@ const createStorageInjectable = getInjectable({
|
||||
directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable),
|
||||
getAbsolutePath: di.inject(getAbsolutePathInjectable),
|
||||
hostedClusterId: di.inject(hostedClusterIdInjectable),
|
||||
saveDelay: di.inject(storageSaveDelayInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ interface Dependencies {
|
||||
writeJsonFile: (filePath: string, contentObject: JsonObject) => Promise<void>;
|
||||
getAbsolutePath: GetAbsolutePath;
|
||||
hostedClusterId: string | undefined;
|
||||
saveDelay: number;
|
||||
}
|
||||
|
||||
export type CreateStorage = <T>(key: string, defaultValue: T) => StorageLayer<T>;
|
||||
@ -36,6 +37,7 @@ export const createStorage = ({
|
||||
readJsonFile,
|
||||
writeJsonFile,
|
||||
hostedClusterId,
|
||||
saveDelay,
|
||||
}: Dependencies): CreateStorage => (key, defaultValue) => {
|
||||
const { logPrefix } = StorageHelper;
|
||||
|
||||
@ -59,7 +61,7 @@ export const createStorage = ({
|
||||
|
||||
// bind auto-saving data changes to %storage-file.json
|
||||
reaction(() => toJS(storage.data), saveFile, {
|
||||
delay: 250, // lazy, avoid excessive writes to fs
|
||||
delay: saveDelay, // lazy, avoid excessive writes to fs
|
||||
equals: comparer.structural, // save only when something really changed
|
||||
});
|
||||
|
||||
|
||||
@ -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 storageSaveDelayInjectable = getInjectable({
|
||||
id: "storage-save-delay",
|
||||
instantiate: () => 250,
|
||||
});
|
||||
|
||||
export default storageSaveDelayInjectable;
|
||||
26
src/renderer/utils/create-storage/storages-are-ready.ts
Normal file
26
src/renderer/utils/create-storage/storages-are-ready.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import type { CreateStorage } from "./create-storage";
|
||||
import createStorageInjectable from "./create-storage.injectable";
|
||||
|
||||
export const controlWhenStoragesAreReady = (di: DiContainer) => {
|
||||
const storagesAreReady: Promise<void>[] = [];
|
||||
|
||||
const decorated =
|
||||
(toBeDecorated: CreateStorage) =>
|
||||
(key: string, defaultValue: any) => {
|
||||
const storage = toBeDecorated(key, defaultValue);
|
||||
|
||||
storagesAreReady.push(storage.whenReady);
|
||||
|
||||
return storage;
|
||||
};
|
||||
|
||||
// TODO: Remove when typing is added to the library
|
||||
(di as any).decorateFunction(createStorageInjectable, decorated);
|
||||
|
||||
return async () => void await Promise.all(storagesAreReady);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user