1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Make navigating to Helm Charts not blow up

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
Iku-turso 2022-04-26 10:39:00 +03:00 committed by Janne Savolainen
parent 203418ac37
commit 823cab8170
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
12 changed files with 779 additions and 186 deletions

View File

@ -0,0 +1,454 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`helm-charts - navigation to Helm charts when navigating to Helm charts renders 1`] = `
<div>
<div
class="flex flex-col"
data-testid="cluster-sidebar"
>
<div>
<div
class=""
style="width: 40px; height: 40px;"
>
??
</div>
<div />
</div>
<div
class="undefined sidebar-active-status"
>
<div
class="SidebarItem"
data-id-test="workloads"
data-is-active-test="false"
data-test-id="workloads"
data-testid="sidebar-item"
>
<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-id-test="config"
data-is-active-test="false"
data-test-id="config"
data-testid="sidebar-item"
>
<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-id-test="network"
data-is-active-test="false"
data-test-id="network"
data-testid="sidebar-item"
>
<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-id-test="storage"
data-is-active-test="false"
data-test-id="storage"
data-testid="sidebar-item"
>
<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-id-test="helm"
data-is-active-test="true"
data-test-id="helm"
data-testid="sidebar-item"
>
<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-id-test="user-management"
data-is-active-test="false"
data-test-id="user-management"
data-testid="sidebar-item"
>
<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-id-test="custom-resources"
data-is-active-test="false"
data-test-id="custom-resources"
data-testid="sidebar-item"
>
<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="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 some-active-theme-type 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_15"
tabindex="0"
>
<span
class="icon"
data-icon-name="more_vert"
>
more_vert
</span>
</i>
</div>
</div>
</div>
<div
class="Spinner singleColor center"
/>
</div>
<div
class="AddRemoveButtons flex gaps"
/>
</div>
</div>
</main>
</div>
</div>
`;

View File

@ -0,0 +1,51 @@
/**
* 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 {
ApplicationBuilder,
getApplicationBuilder,
} from "../../renderer/components/test-utils/get-application-builder";
describe("helm-charts - navigation to Helm charts", () => {
let applicationBuilder: ApplicationBuilder;
beforeEach(() => {
applicationBuilder = getApplicationBuilder();
// applicationBuilder.beforeSetups(({ rendererDi }) => {
// const userStoreStub = {
// extensionRegistryUrl: { customUrl: "some-custom-url" },
// } as unknown as UserStore;
//
// rendererDi.override(userStoreInjectable, () => userStoreStub);
//
// const themeStoreStub = ({ themeOptions: [] }) as unknown as ThemeStore;
//
// rendererDi.override(themeStoreInjectable, () => themeStoreStub);
// });
});
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();
});
});
});

View File

@ -66,6 +66,8 @@ import getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get-
import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable"; import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable";
import setupMainWindowVisibilityAfterActivationInjectable from "./electron-app/after-application-is-ready/setup-main-window-visibility-after-activation.injectable"; import setupMainWindowVisibilityAfterActivationInjectable from "./electron-app/after-application-is-ready/setup-main-window-visibility-after-activation.injectable";
import setupDeviceShutdownInjectable from "./electron-app/after-application-is-ready/setup-device-shutdown.injectable"; import setupDeviceShutdownInjectable from "./electron-app/after-application-is-ready/setup-device-shutdown.injectable";
import setupApplicationNameInjectable from "./electron-app/before-application-is-ready/setup-application-name.injectable";
import setupRunnablesBeforeClosingOfApplicationInjectable from "./electron-app/before-application-is-ready/setup-runnables-before-closing-of-application.injectable";
export const getDiForUnitTesting = ( export const getDiForUnitTesting = (
{ doGeneralOverrides } = { doGeneralOverrides: false }, { doGeneralOverrides } = { doGeneralOverrides: false },
@ -195,6 +197,8 @@ const overrideElectronFeatures = (di: DiContainer) => {
di.override(setupDeepLinkingInjectable, () => ({ run: () => {} })); di.override(setupDeepLinkingInjectable, () => ({ run: () => {} }));
di.override(exitAppInjectable, () => () => {}); di.override(exitAppInjectable, () => () => {});
di.override(setupApplicationNameInjectable, () => ({ run: () => {} }));
di.override(setupRunnablesBeforeClosingOfApplicationInjectable, () => ({ run: () => {} }));
di.override(getCommandLineSwitchInjectable, () => () => "irrelevant"); di.override(getCommandLineSwitchInjectable, () => () => "irrelevant");
di.override(requestSingleInstanceLockInjectable, () => () => true); di.override(requestSingleInstanceLockInjectable, () => () => true);
di.override(disableHardwareAccelerationInjectable, () => () => {}); di.override(disableHardwareAccelerationInjectable, () => () => {});

View File

@ -17,14 +17,11 @@ describe("Helm Service tests", () => {
it("list charts with deprecated entries", async () => { it("list charts with deprecated entries", async () => {
mockHelmRepoManager.mockReturnValue({ mockHelmRepoManager.mockReturnValue({
init: jest.fn(), repositories: jest.fn().mockImplementation(async () => [
repositories: jest.fn().mockImplementation(async () => {
return [
{ name: "stable", url: "stableurl" }, { name: "stable", url: "stableurl" },
{ name: "experiment", url: "experimenturl" }, { name: "experiment", url: "experimenturl" },
]; ]),
}), } as any);
});
const charts = await helmService.listCharts(); const charts = await helmService.listCharts();
@ -127,13 +124,12 @@ describe("Helm Service tests", () => {
it("list charts sorted by version in descending order", async () => { it("list charts sorted by version in descending order", async () => {
mockHelmRepoManager.mockReturnValue({ mockHelmRepoManager.mockReturnValue({
init: jest.fn(),
repositories: jest.fn().mockImplementation(async () => { repositories: jest.fn().mockImplementation(async () => {
return [ return [
{ name: "bitnami", url: "bitnamiurl" }, { name: "bitnami", url: "bitnamiurl" },
]; ];
}), }),
}); } as any);
const charts = await helmService.listCharts(); const charts = await helmService.listCharts();

View File

@ -3,16 +3,161 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { HelmRepoManager } from "./helm-repo-manager";
import yaml from "js-yaml";
import { readFile } from "fs-extra";
import { customRequestPromise } from "../../common/request";
import orderBy from "lodash/orderBy";
import logger from "../logger";
import { execHelm } from "./exec";
import type { HelmEnv, HelmRepo, HelmRepoConfig } from "./helm-repo-manager";
class HelmRepoManager {
protected repos: HelmRepo[];
protected helmEnv: HelmEnv;
protected didUpdateOnce: boolean;
public async loadAvailableRepos(): Promise<HelmRepo[]> {
const res = await customRequestPromise({
uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json",
json: true,
resolveWithFullResponse: true,
timeout: 10000,
});
return orderBy(res.body as HelmRepo[], (repo) => repo.name);
}
private async ensureInitialized() {
this.helmEnv ??= await this.parseHelmEnv();
const repos = await this.list();
if (repos.length === 0) {
await this.addRepo({
name: "bitnami",
url: "https://charts.bitnami.com/bitnami",
});
}
if (!this.didUpdateOnce) {
await this.update();
this.didUpdateOnce = true;
}
}
protected async parseHelmEnv() {
const output = await execHelm(["env"]);
const lines = output.split(/\r?\n/); // split by new line feed
const env: HelmEnv = {};
lines.forEach((line: string) => {
const [key, value] = line.split("=");
if (key && value) {
env[key] = value.replace(/"/g, ""); // strip quotas
}
});
return env;
}
public async repo(name: string): Promise<HelmRepo> {
const repos = await this.repositories();
return repos.find((repo) => repo.name === name);
}
private async list(): Promise<HelmRepo[]> {
try {
const rawConfig = await readFile(
this.helmEnv.HELM_REPOSITORY_CONFIG,
"utf8",
);
const parsedConfig = yaml.load(rawConfig) as HelmRepoConfig;
if (typeof parsedConfig === "object" && parsedConfig) {
return parsedConfig.repositories;
}
} catch {
// ignore error
}
return [];
}
public async repositories(): Promise<HelmRepo[]> {
try {
await this.ensureInitialized();
const repos = await this.list();
return repos.map((repo) => ({
...repo,
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`,
}));
} catch (error) {
logger.error(`[HELM]: repositories listing error`, error);
return [];
}
}
public async update() {
return execHelm(["repo", "update"]);
}
public async addRepo({
name,
url,
insecureSkipTlsVerify,
username,
password,
caFile,
keyFile,
certFile,
}: HelmRepo) {
logger.info(`[HELM]: adding repo ${name} from ${url}`);
const args = ["repo", "add", name, url];
if (insecureSkipTlsVerify) {
args.push("--insecure-skip-tls-verify");
}
if (username) {
args.push("--username", username);
}
if (password) {
args.push("--password", password);
}
if (caFile) {
args.push("--ca-file", caFile);
}
if (keyFile) {
args.push("--key-file", keyFile);
}
if (certFile) {
args.push("--cert-file", certFile);
}
return execHelm(args);
}
public async removeRepo({ name, url }: HelmRepo): Promise<string> {
logger.info(`[HELM]: removing repo ${name} (${url})`);
return execHelm(["repo", "remove", name]);
}
}
const helmRepoManagerInjectable = getInjectable({ const helmRepoManagerInjectable = getInjectable({
id: "helm-repo-manager", id: "helm-repo-manager",
instantiate: () => { instantiate: () => new HelmRepoManager(),
HelmRepoManager.resetInstance();
return HelmRepoManager.createInstance();
},
}); });
export default helmRepoManagerInjectable; export default helmRepoManagerInjectable;

View File

@ -3,13 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import yaml from "js-yaml"; import {
import { readFile } from "fs-extra"; asLegacyGlobalSingletonForExtensionApi,
import { Singleton } from "../../common/utils/singleton"; } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api";
import { customRequestPromise } from "../../common/request";
import orderBy from "lodash/orderBy"; import helmRepoManagerInjectable from "./helm-repo-manager.injectable";
import logger from "../logger";
import { execHelm } from "./exec";
export type HelmEnv = Record<string, string> & { export type HelmEnv = Record<string, string> & {
HELM_REPOSITORY_CACHE?: string; HELM_REPOSITORY_CACHE?: string;
@ -32,144 +30,4 @@ export interface HelmRepo {
password?: string; password?: string;
} }
export class HelmRepoManager extends Singleton { export const HelmRepoManager = asLegacyGlobalSingletonForExtensionApi(helmRepoManagerInjectable);
protected repos: HelmRepo[];
protected helmEnv: HelmEnv;
protected didUpdateOnce: boolean;
public async loadAvailableRepos(): Promise<HelmRepo[]> {
const res = await customRequestPromise({
uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json",
json: true,
resolveWithFullResponse: true,
timeout: 10000,
});
return orderBy(res.body as HelmRepo[], repo => repo.name);
}
private async ensureInitialized() {
this.helmEnv ??= await this.parseHelmEnv();
const repos = await this.list();
if (repos.length === 0) {
await this.addRepo({
name: "bitnami",
url: "https://charts.bitnami.com/bitnami",
});
}
if (!this.didUpdateOnce) {
await this.update();
this.didUpdateOnce = true;
}
}
protected async parseHelmEnv() {
const output = await execHelm(["env"]);
const lines = output.split(/\r?\n/); // split by new line feed
const env: HelmEnv = {};
lines.forEach((line: string) => {
const [key, value] = line.split("=");
if (key && value) {
env[key] = value.replace(/"/g, ""); // strip quotas
}
});
return env;
}
public async repo(name: string): Promise<HelmRepo> {
const repos = await this.repositories();
return repos.find(repo => repo.name === name);
}
private async list(): Promise<HelmRepo[]> {
try {
const rawConfig = await readFile(this.helmEnv.HELM_REPOSITORY_CONFIG, "utf8");
const parsedConfig = yaml.load(rawConfig) as HelmRepoConfig;
if (typeof parsedConfig === "object" && parsedConfig) {
return parsedConfig.repositories;
}
} catch {
// ignore error
}
return [];
}
public async repositories(): Promise<HelmRepo[]> {
try {
await this.ensureInitialized();
const repos = await this.list();
return repos.map(repo => ({
...repo,
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`,
}));
} catch (error) {
logger.error(`[HELM]: repositories listing error`, error);
return [];
}
}
public async update() {
return execHelm([
"repo",
"update",
]);
}
public async addRepo({ name, url, insecureSkipTlsVerify, username, password, caFile, keyFile, certFile }: HelmRepo) {
logger.info(`[HELM]: adding repo ${name} from ${url}`);
const args = [
"repo",
"add",
name,
url,
];
if (insecureSkipTlsVerify) {
args.push("--insecure-skip-tls-verify");
}
if (username) {
args.push("--username", username);
}
if (password) {
args.push("--password", password);
}
if (caFile) {
args.push("--ca-file", caFile);
}
if (keyFile) {
args.push("--key-file", keyFile);
}
if (certFile) {
args.push("--cert-file", certFile);
}
return execHelm(args);
}
public async removeRepo({ name, url }: HelmRepo): Promise<string> {
logger.info(`[HELM]: removing repo ${name} (${url})`);
return execHelm([
"repo",
"remove",
name,
]);
}
}

View File

@ -74,6 +74,8 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
render() { render() {
return ( return (
<SiblingsInTabLayout> <SiblingsInTabLayout>
<div data-testid="page-for-helm-charts" style={{ display: "none" }}/>
<ItemListLayout <ItemListLayout
isConfigurable isConfigurable
tableId="helm_charts" tableId="helm_charts"

View File

@ -21,7 +21,7 @@ import { Spinner } from "../../spinner";
import { Table, TableCell, TableHead, TableRow } from "../../table"; import { Table, TableCell, TableHead, TableRow } from "../../table";
import { Button } from "../../button"; import { Button } from "../../button";
import { Notifications } from "../../notifications"; import { Notifications } from "../../notifications";
import { ThemeStore } from "../../../theme.store"; import type { ThemeStore } from "../../../theme.store";
import { apiManager } from "../../../../common/k8s-api/api-manager"; import { apiManager } from "../../../../common/k8s-api/api-manager";
import { SubTitle } from "../../layout/sub-title"; import { SubTitle } from "../../layout/sub-title";
import { getDetailsUrl } from "../../kube-detail-params"; import { getDetailsUrl } from "../../kube-detail-params";
@ -37,6 +37,7 @@ import releaseValuesInjectable from "./release-values.injectable";
import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-shown.injectable"; import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-shown.injectable";
import type { KubeObject } from "../../../../common/k8s-api/kube-object"; import type { KubeObject } from "../../../../common/k8s-api/kube-object";
import { KubeObjectAge } from "../../kube-object/age"; import { KubeObjectAge } from "../../kube-object/age";
import themeStoreInjectable from "../../../theme-store.injectable";
export interface ReleaseDetailsProps { export interface ReleaseDetailsProps {
hideDetails(): void; hideDetails(): void;
@ -49,6 +50,7 @@ interface Dependencies {
updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise<IReleaseUpdateDetails>; updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise<IReleaseUpdateDetails>;
createUpgradeChartTab: (release: HelmRelease) => void; createUpgradeChartTab: (release: HelmRelease) => void;
userSuppliedValuesAreShown: { toggle: () => void; value: boolean }; userSuppliedValuesAreShown: { toggle: () => void; value: boolean };
themeStore: ThemeStore;
} }
@observer @observer
@ -249,13 +251,13 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
} }
render() { render() {
const { hideDetails } = this.props; const { hideDetails, themeStore } = this.props;
const title = this.release ? `Release: ${this.release.getName()}` : ""; const title = this.release ? `Release: ${this.release.getName()}` : "";
const toolbar = <HelmReleaseMenu release={this.release} toolbar hideDetails={hideDetails}/>; const toolbar = <HelmReleaseMenu release={this.release} toolbar hideDetails={hideDetails}/>;
return ( return (
<Drawer <Drawer
className={cssNames("ReleaseDetails", ThemeStore.getInstance().activeTheme.type)} className={cssNames("ReleaseDetails", themeStore.activeTheme.type)}
usePortal={true} usePortal={true}
open={!!this.release} open={!!this.release}
title={title} title={title}
@ -281,6 +283,8 @@ export const ReleaseDetails = withInjectables<Dependencies, ReleaseDetailsProps>
updateRelease: di.inject(updateReleaseInjectable), updateRelease: di.inject(updateReleaseInjectable),
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable), createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
themeStore: di.inject(themeStoreInjectable),
...props, ...props,
}), }),
}, },

View File

@ -21,13 +21,16 @@ import { Spinner } from "../spinner";
import type { ItemObject, ItemStore } from "../../../common/item.store"; import type { ItemObject, ItemStore } from "../../../common/item.store";
import type { Filter } from "./page-filters.store"; import type { Filter } from "./page-filters.store";
import { pageFilters } from "./page-filters.store"; import { pageFilters } from "./page-filters.store";
import { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../theme.store";
import { MenuActions } from "../menu/menu-actions"; import { MenuActions } from "../menu/menu-actions";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
import { Checkbox } from "../checkbox"; import { Checkbox } from "../checkbox";
import { UserStore } from "../../../common/user-store"; import type { UserStore } from "../../../common/user-store";
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
import themeStoreInjectable from "../../theme-store.injectable";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import openConfirmDialogInjectable from "../confirm-dialog/open.injectable"; import openConfirmDialogInjectable from "../confirm-dialog/open.injectable";
export interface ItemListLayoutContentProps<I extends ItemObject> { export interface ItemListLayoutContentProps<I extends ItemObject> {
@ -66,12 +69,14 @@ export interface ItemListLayoutContentProps<I extends ItemObject> {
} }
interface Dependencies { interface Dependencies {
themeStore: ThemeStore;
userStore: UserStore;
openConfirmDialog: OpenConfirmDialog; openConfirmDialog: OpenConfirmDialog;
} }
@observer @observer
class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Component<ItemListLayoutContentProps<I> & Dependencies> { class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Component<Dependencies & ItemListLayoutContentProps<I>> {
constructor(props: ItemListLayoutContentProps<I> & Dependencies) { constructor(props: Dependencies & ItemListLayoutContentProps<I>) {
super(props); super(props);
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
@ -255,7 +260,7 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
detailsItem, className, tableProps = {}, tableId, getItems, detailsItem, className, tableProps = {}, tableId, getItems,
} = this.props; } = this.props;
const selectedItemId = detailsItem && detailsItem.getId(); const selectedItemId = detailsItem && detailsItem.getId();
const classNames = cssNames(className, "box", "grow", ThemeStore.getInstance().activeTheme.type); const classNames = cssNames(className, "box", "grow", this.props.themeStore.activeTheme.type);
const items = getItems(); const items = getItems();
const selectedItems = store.pickOnlySelected(items); const selectedItems = store.pickOnlySelected(items);
@ -298,7 +303,7 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
showColumn({ id: columnId, showWithColumn }: TableCellProps): boolean { showColumn({ id: columnId, showWithColumn }: TableCellProps): boolean {
const { tableId, isConfigurable } = this.props; const { tableId, isConfigurable } = this.props;
return !isConfigurable || !UserStore.getInstance().isTableColumnHidden(tableId, columnId, showWithColumn); return !isConfigurable || !this.props.userStore.isTableColumnHidden(tableId, columnId, showWithColumn);
} }
renderColumnVisibilityMenu() { renderColumnVisibilityMenu() {
@ -312,7 +317,7 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
<Checkbox <Checkbox
label={cellProps.title ?? `<${cellProps.className}>`} label={cellProps.title ?? `<${cellProps.className}>`}
value={this.showColumn(cellProps)} value={this.showColumn(cellProps)}
onChange={() => UserStore.getInstance().toggleTableColumnVisibility(tableId, cellProps.id)} onChange={() => this.props.userStore.toggleTableColumnVisibility(tableId, cellProps.id)}
/> />
</MenuItem> </MenuItem>
) )
@ -322,9 +327,20 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
} }
} }
export const ItemListLayoutContent = withInjectables<Dependencies, ItemListLayoutContentProps<ItemObject>>(NonInjectedItemListLayoutContent, { const InjectedItemListLayoutContent = withInjectables<Dependencies, ItemListLayoutContentProps<ItemObject>>(
NonInjectedItemListLayoutContent,
{
getProps: (di, props) => ({ getProps: (di, props) => ({
...props, themeStore: di.inject(themeStoreInjectable),
userStore: di.inject(userStoreInjectable),
openConfirmDialog: di.inject(openConfirmDialogInjectable), openConfirmDialog: di.inject(openConfirmDialogInjectable),
...props,
}), }),
}) as <I extends ItemObject>(props: ItemListLayoutContentProps<I>) => React.ReactElement; },
);
export function ItemListLayoutContent<I extends ItemObject>(
props: ItemListLayoutContentProps<I>,
) {
return <InjectedItemListLayoutContent {...props} />;
}

View File

@ -18,9 +18,11 @@ import ReactSelect, {
type StylesConfig, type StylesConfig,
} from "react-select"; } from "react-select";
import ReactSelectCreatable, { type CreatableProps } from "react-select/creatable"; import ReactSelectCreatable, { type CreatableProps } from "react-select/creatable";
import { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../theme.store";
import { autoBind, cssNames } from "../../utils"; import { autoBind, cssNames } from "../../utils";
import { merge } from "lodash"; import { merge } from "lodash";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../theme-store.injectable";
const { Menu } = components; const { Menu } = components;
@ -34,6 +36,10 @@ export interface SelectOption<T = any> {
label?: React.ReactNode; label?: React.ReactNode;
} }
interface Dependencies {
themeStore: ThemeStore;
}
export interface SelectProps<T = any> extends ReactSelectProps<T, boolean>, CreatableProps<T, boolean, GroupBase<T>> { export interface SelectProps<T = any> extends ReactSelectProps<T, boolean>, CreatableProps<T, boolean, GroupBase<T>> {
id?: string; // Optional only because of Extension API. Required to make Select deterministic in unit tests id?: string; // Optional only because of Extension API. Required to make Select deterministic in unit tests
value?: T; value?: T;
@ -45,21 +51,21 @@ export interface SelectProps<T = any> extends ReactSelectProps<T, boolean>, Crea
} }
@observer @observer
export class Select extends React.Component<SelectProps> { class NonInjectedSelect extends React.Component<SelectProps & Dependencies> {
static defaultProps: Omit<SelectProps, "id"> = { static defaultProps: Omit<SelectProps, "id"> = {
autoConvertOptions: true, autoConvertOptions: true,
menuPortalTarget: document.body, menuPortalTarget: document.body,
menuPlacement: "auto", menuPlacement: "auto",
}; };
constructor(props: SelectProps) { constructor(props: SelectProps & Dependencies) {
super(props); super(props);
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
} }
@computed get themeClass() { @computed get themeClass() {
const themeName = this.props.themeName || ThemeStore.getInstance().activeTheme.type; const themeName = this.props.themeName || this.props.themeStore.activeTheme.type;
return `theme-${themeName}`; return `theme-${themeName}`;
} }
@ -153,3 +159,14 @@ export class Select extends React.Component<SelectProps> {
: <ReactSelect {...selectProps} />; : <ReactSelect {...selectProps} />;
} }
} }
export const Select = withInjectables<Dependencies, SelectProps>(
NonInjectedSelect,
{
getProps: (di, props) => ({
themeStore: di.inject(themeStoreInjectable),
...props,
}),
},
);

View File

@ -33,6 +33,10 @@ import preferenceNavigationItemsInjectable from "../+preferences/preferences-nav
import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import type { MenuItemOpts } from "../../../main/menu/application-menu-items.injectable"; import type { MenuItemOpts } from "../../../main/menu/application-menu-items.injectable";
import applicationMenuItemsInjectable from "../../../main/menu/application-menu-items.injectable"; import applicationMenuItemsInjectable from "../../../main/menu/application-menu-items.injectable";
import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-frame-context.injectable";
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster.injectable";
type Callback = (dis: DiContainers) => void | Promise<void>; type Callback = (dis: DiContainers) => void | Promise<void>;
@ -56,6 +60,10 @@ export interface ApplicationBuilder {
click: (id: string) => void; click: (id: string) => void;
}; };
}; };
helmCharts: {
navigate: () => void;
};
} }
interface DiContainers { interface DiContainers {
@ -192,6 +200,14 @@ export const getApplicationBuilder = () => {
}, },
}, },
helmCharts: {
navigate: () => {
const navigateToHelmCharts = rendererDi.inject(navigateToHelmChartsInjectable);
navigateToHelmCharts();
},
},
setEnvironmentToClusterFrame: () => { setEnvironmentToClusterFrame: () => {
environment = environments.clusterFrame; environment = environments.clusterFrame;
@ -206,6 +222,17 @@ export const getApplicationBuilder = () => {
() => "/irrelevant", () => "/irrelevant",
); );
rendererDi.override(hostedClusterInjectable, () => ({
accessibleNamespaces: [],
}));
const clusterFrameContext = rendererDi.inject(
clusterFrameContextInjectable,
);
// Todo: get rid of global state.
KubeObjectStore.defaultContext.set(clusterFrameContext);
return builder; return builder;
}, },

View File

@ -33,6 +33,9 @@ import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake
import joinPathsInjectable from "../common/path/join-paths.injectable"; import joinPathsInjectable from "../common/path/join-paths.injectable";
import { joinPathsFake } from "../common/test-utils/join-paths-fake"; import { joinPathsFake } from "../common/test-utils/join-paths-fake";
import hotbarStoreInjectable from "../common/hotbar-store.injectable"; import hotbarStoreInjectable from "../common/hotbar-store.injectable";
import themeStoreInjectable from "./theme-store.injectable";
import apiManagerInjectable from "./components/kube-object-menu/dependencies/api-manager.injectable";
import { ApiManager } from "../common/k8s-api/api-manager";
export const getDiForUnitTesting = ( export const getDiForUnitTesting = (
{ doGeneralOverrides } = { doGeneralOverrides: false }, { doGeneralOverrides } = { doGeneralOverrides: false },
@ -69,7 +72,16 @@ export const getDiForUnitTesting = (
// eslint-disable-next-line unused-imports/no-unused-vars-ts // eslint-disable-next-line unused-imports/no-unused-vars-ts
di.override(clusterStoreInjectable, () => ({ getById: (id): Cluster => ({}) as Cluster }) as ClusterStore); di.override(clusterStoreInjectable, () => ({ getById: (id): Cluster => ({}) as Cluster }) as ClusterStore);
di.override(userStoreInjectable, () => ({}) as UserStore);
di.override(
userStoreInjectable,
() =>
({
isTableColumnHidden: () => false,
} as unknown as UserStore),
);
di.override(apiManagerInjectable, () => new ApiManager());
di.override(getValueFromRegisteredChannelInjectable, () => () => undefined); di.override(getValueFromRegisteredChannelInjectable, () => () => undefined);
di.override(registerIpcChannelListenerInjectable, () => () => undefined); di.override(registerIpcChannelListenerInjectable, () => () => undefined);
@ -92,6 +104,13 @@ export const getDiForUnitTesting = (
error: (message: string, ...args: any) => console.error(message, ...args), error: (message: string, ...args: any) => console.error(message, ...args),
info: noop, info: noop,
})); }));
di.override(themeStoreInjectable, () => ({
activeTheme: {
type: "some-active-theme-type",
},
}));
} }
return di; return di;