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

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

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-04-21 10:18:50 -04:00
parent 48d5aeb18c
commit 955aa28bc8
44 changed files with 406 additions and 238 deletions

View File

@ -270,16 +270,11 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="mt-4 mb-5 leading-relaxed" class="mt-4 mb-5 leading-relaxed"
> >
This setting is to change the registry URL for installing extensions by name. This setting is to change the registry URL for installing extensions by name.
If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
If you are unable to access the default registry (
https://registry.npmjs.org
)
you can change it in your
<b> <b>
.npmrc .npmrc
</b> </b>
 file or in the input below. file or in the input below.
</p> </p>
<div <div
class="Input theme round black disabled invalid" class="Input theme round black disabled invalid"

View File

@ -12,8 +12,8 @@ import { routeInjectionToken } from "../../common/front-end-routing/route-inject
import { computed } from "mobx"; import { computed } from "mobx";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store"; import type { ThemeStore } from "../../renderer/themes/store";
import themeStoreInjectable from "../../renderer/theme-store.injectable"; import themeStoreInjectable from "../../renderer/themes/store.injectable";
import { preferenceNavigationItemInjectionToken } from "../../renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable"; import { preferenceNavigationItemInjectionToken } from "../../renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../renderer/routes/route-is-active.injectable"; import routeIsActiveInjectable from "../../renderer/routes/route-is-active.injectable";
import { Preferences } from "../../renderer/components/+preferences"; import { Preferences } from "../../renderer/components/+preferences";

View File

@ -7,8 +7,8 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable"; import themeStoreInjectable from "../../renderer/themes/store.injectable";
import type { ThemeStore } from "../../renderer/theme.store"; import type { ThemeStore } from "../../renderer/themes/store";
import navigateToProxyPreferencesInjectable import navigateToProxyPreferencesInjectable
from "../../common/front-end-routing/routes/preferences/proxy/navigate-to-proxy-preferences.injectable"; from "../../common/front-end-routing/routes/preferences/proxy/navigate-to-proxy-preferences.injectable";

View File

@ -7,8 +7,8 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable"; import themeStoreInjectable from "../../renderer/themes/store.injectable";
import type { ThemeStore } from "../../renderer/theme.store"; import type { ThemeStore } from "../../renderer/themes/store";
describe("preferences - navigation to editor preferences", () => { describe("preferences - navigation to editor preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;

View File

@ -7,9 +7,9 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable"; import themeStoreInjectable from "../../renderer/themes/store.injectable";
import type { ThemeStore } from "../../renderer/theme.store"; import type { ThemeStore } from "../../renderer/themes/store";
import type { LensRendererExtension } from "../../extensions/lens-renderer-extension"; import { LensRendererExtension } from "../../extensions/lens-renderer-extension";
import React from "react"; import React from "react";
import { getRendererExtensionFake } from "../../renderer/components/test-utils/get-renderer-extension-fake"; import { getRendererExtensionFake } from "../../renderer/components/test-utils/get-renderer-extension-fake";
@ -107,10 +107,8 @@ describe("preferences - navigation to extension specific preferences", () => {
}); });
}); });
const extensionStubWithExtensionSpecificPreferenceItems: Partial<LensRendererExtension> = { const extensionStubWithExtensionSpecificPreferenceItems = new (class extends LensRendererExtension{
id: "some-test-extension-id", appPreferences = [
appPreferences: [
{ {
title: "Some preference item", title: "Some preference item",
id: "some-preference-item-id", id: "some-preference-item-id",
@ -131,6 +129,17 @@ const extensionStubWithExtensionSpecificPreferenceItems: Partial<LensRendererExt
Input: () => <div />, Input: () => <div />,
}, },
}, },
], ];
}; })({
absolutePath: "/some/absolute/path",
id: "some-extension-id",
isBundled: true,
isCompatible: true,
isEnabled: true,
manifest: {
name: "some-extension-name",
version: "1.0.0",
},
manifestPath: "/some/manifest/path",
});

View File

@ -7,8 +7,8 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable"; import themeStoreInjectable from "../../renderer/themes/store.injectable";
import type { ThemeStore } from "../../renderer/theme.store"; import type { ThemeStore } from "../../renderer/themes/store";
import { observable } from "mobx"; import { observable } from "mobx";
describe("preferences - navigation to kubernetes preferences", () => { describe("preferences - navigation to kubernetes preferences", () => {

View File

@ -7,8 +7,8 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable"; import themeStoreInjectable from "../../renderer/themes/store.injectable";
import type { ThemeStore } from "../../renderer/theme.store"; import type { ThemeStore } from "../../renderer/themes/store";
describe("preferences - navigation to proxy preferences", () => { describe("preferences - navigation to proxy preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;

View File

@ -9,8 +9,8 @@ import { getApplicationBuilder } from "../../renderer/components/test-utils/get-
import { getRendererExtensionFake } from "../../renderer/components/test-utils/get-renderer-extension-fake"; import { getRendererExtensionFake } from "../../renderer/components/test-utils/get-renderer-extension-fake";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store"; import type { ThemeStore } from "../../renderer/themes/store";
import themeStoreInjectable from "../../renderer/theme-store.injectable"; import themeStoreInjectable from "../../renderer/themes/store.injectable";
import navigateToTelemetryPreferencesInjectable from "../../common/front-end-routing/routes/preferences/telemetry/navigate-to-telemetry-preferences.injectable"; import navigateToTelemetryPreferencesInjectable from "../../common/front-end-routing/routes/preferences/telemetry/navigate-to-telemetry-preferences.injectable";
import sentryDnsUrlInjectable from "../../renderer/components/+preferences/sentry-dns-url.injectable"; import sentryDnsUrlInjectable from "../../renderer/components/+preferences/sentry-dns-url.injectable";

View File

@ -7,10 +7,10 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import { observable } from "mobx"; import { observable } from "mobx";
import defaultShellInjectable from "../../renderer/components/+preferences/default-shell.injectable"; import defaultShellInjectable from "../../renderer/components/+preferences/default-shell.injectable";
import themeStoreInjectable from "../../renderer/themes/store.injectable";
import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
describe("preferences - navigation to terminal preferences", () => { describe("preferences - navigation to terminal preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -26,12 +26,12 @@ describe("preferences - navigation to terminal preferences", () => {
} as unknown as UserStore; } as unknown as UserStore;
rendererDi.override(userStoreInjectable, () => userStoreStub); rendererDi.override(userStoreInjectable, () => userStoreStub);
rendererDi.override(defaultShellInjectable, () => "some-default-shell"); rendererDi.override(defaultShellInjectable, () => "some-default-shell");
rendererDi.permitSideEffects(themeStoreInjectable);
const themeStoreStub = ({ themeOptions: [] }) as unknown as ThemeStore; rendererDi.override(ipcRendererInjectable, () => ({
on: jest.fn(),
rendererDi.override(themeStoreInjectable, () => themeStoreStub); invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
} as never));
}); });
}); });

View File

@ -9,8 +9,8 @@ import { getApplicationBuilder } from "../../renderer/components/test-utils/get-
import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable"; import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable";
import type { UserStore } from "../../common/user-store"; import type { UserStore } from "../../common/user-store";
import userStoreInjectable from "../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store"; import type { ThemeStore } from "../../renderer/themes/store";
import themeStoreInjectable from "../../renderer/theme-store.injectable"; import themeStoreInjectable from "../../renderer/themes/store.injectable";
describe("preferences - navigation using application menu", () => { describe("preferences - navigation using application menu", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;

View File

@ -30,7 +30,7 @@ import userStoreInjectable from "../user-store/user-store.injectable";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import type { ClusterStoreModel } from "../cluster-store/cluster-store"; import type { ClusterStoreModel } from "../cluster-store/cluster-store";
import { defaultTheme } from "../vars"; import { defaultThemeId } from "../vars";
import writeFileInjectable from "../fs/write-file.injectable"; import writeFileInjectable from "../fs/write-file.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable import getConfigurationFileModelInjectable
@ -80,7 +80,7 @@ describe("user store tests", () => {
userStore.httpsProxy = "abcd://defg"; userStore.httpsProxy = "abcd://defg";
expect(userStore.httpsProxy).toBe("abcd://defg"); expect(userStore.httpsProxy).toBe("abcd://defg");
expect(userStore.colorTheme).toBe(defaultTheme); expect(userStore.colorTheme).toBe(defaultThemeId);
userStore.colorTheme = "light"; userStore.colorTheme = "light";
expect(userStore.colorTheme).toBe("light"); expect(userStore.colorTheme).toBe("light");
@ -89,7 +89,7 @@ describe("user store tests", () => {
it("correctly resets theme to default value", async () => { it("correctly resets theme to default value", async () => {
userStore.colorTheme = "some other theme"; userStore.colorTheme = "some other theme";
userStore.resetTheme(); userStore.resetTheme();
expect(userStore.colorTheme).toBe(defaultTheme); expect(userStore.colorTheme).toBe(defaultThemeId);
}); });
it("correctly calculates if the last seen version is an old release", () => { it("correctly calculates if the last seen version is an old release", () => {

View File

@ -10,7 +10,7 @@ import { getAppVersion } from "../utils";
import type { editor } from "monaco-editor"; import type { editor } from "monaco-editor";
import merge from "lodash/merge"; import merge from "lodash/merge";
import { SemVer } from "semver"; import { SemVer } from "semver";
import { defaultTheme, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars"; import { defaultThemeId, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars";
import type { ObservableMap } from "mobx"; import type { ObservableMap } from "mobx";
import { observable } from "mobx"; import { observable } from "mobx";
import { readonly } from "../utils/readonly"; import { readonly } from "../utils/readonly";
@ -77,10 +77,10 @@ const shell: PreferenceDescription<string | undefined> = {
const colorTheme: PreferenceDescription<string> = { const colorTheme: PreferenceDescription<string> = {
fromStore(val) { fromStore(val) {
return val || defaultTheme; return val || defaultThemeId;
}, },
toStore(val) { toStore(val) {
if (!val || val === defaultTheme) { if (!val || val === defaultThemeId) {
return undefined; return undefined;
} }

View File

@ -7,6 +7,7 @@
import path from "path"; import path from "path";
import { SemVer } from "semver"; import { SemVer } from "semver";
import packageInfo from "../../package.json"; import packageInfo from "../../package.json";
import type { ThemeId } from "../renderer/themes/store";
import { lazyInitialized } from "./utils/lazy-initialized"; import { lazyInitialized } from "./utils/lazy-initialized";
export const isMac = process.platform === "darwin"; export const isMac = process.platform === "darwin";
@ -25,7 +26,7 @@ export const isIntegrationTesting = process.argv.includes(integrationTestingArg)
export const productName = packageInfo.productName; export const productName = packageInfo.productName;
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`; export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
export const publicPath = "/build/" as string; export const publicPath = "/build/" as string;
export const defaultTheme = "lens-dark" as string; export const defaultThemeId: ThemeId = "lens-dark";
export const defaultFontSize = 12; export const defaultFontSize = 12;
export const defaultTerminalFontFamily = "RobotoMono"; export const defaultTerminalFontFamily = "RobotoMono";
export const defaultEditorFontFamily = "RobotoMono"; export const defaultEditorFontFamily = "RobotoMono";

View File

@ -3,8 +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 { ThemeStore } from "../../renderer/theme.store"; import themeStoreInjectable from "../../renderer/themes/store.injectable";
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
const themeStore = asLegacyGlobalForExtensionApi(themeStoreInjectable);
export function getActiveTheme() { export function getActiveTheme() {
return ThemeStore.getInstance().activeTheme; return themeStore.activeTheme;
} }

View File

@ -5,12 +5,16 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import ipcRendererInjectable from "./ipc-renderer/ipc-renderer.injectable"; import ipcRendererInjectable from "./ipc-renderer/ipc-renderer.injectable";
import { getValueFromRegisteredChannel } from "./get-value-from-registered-channel"; import { getValueFromRegisteredChannel } from "./get-value-from-registered-channel";
import type { Channel } from "../../../common/ipc-channel/channel";
export type GetValueFromRegisteredChannel = <TChannel extends Channel<TInstance>, TInstance>(channel: TChannel) => Promise<TChannel["_template"]>;
const getValueFromRegisteredChannelInjectable = getInjectable({ const getValueFromRegisteredChannelInjectable = getInjectable({
id: "get-value-from-registered-channel", id: "get-value-from-registered-channel",
instantiate: (di) => instantiate: (di) => getValueFromRegisteredChannel({
getValueFromRegisteredChannel({ ipcRenderer: di.inject(ipcRendererInjectable) }), ipcRenderer: di.inject(ipcRendererInjectable),
}),
}); });
export default getValueFromRegisteredChannelInjectable; export default getValueFromRegisteredChannelInjectable;

View File

@ -9,8 +9,7 @@ interface Dependencies {
ipcRenderer: IpcRenderer; ipcRenderer: IpcRenderer;
} }
export const getValueFromRegisteredChannel = export const getValueFromRegisteredChannel = ({ ipcRenderer }: Dependencies) =>
({ ipcRenderer }: Dependencies) =>
<TChannel extends Channel<TInstance>, TInstance>( <TChannel extends Channel<TInstance>, TInstance>(
channel: TChannel, channel: TChannel,
): Promise<TChannel["_template"]> => ): Promise<TChannel["_template"]> =>

View File

@ -37,7 +37,7 @@ import initClusterFrameInjectable from "./frames/cluster-frame/init-cluster-fram
import commandOverlayInjectable from "./components/command-palette/command-overlay.injectable"; import commandOverlayInjectable from "./components/command-palette/command-overlay.injectable";
import { Router } from "react-router"; import { Router } from "react-router";
import historyInjectable from "./navigation/history.injectable"; import historyInjectable from "./navigation/history.injectable";
import themeStoreInjectable from "./theme-store.injectable"; import themeStoreInjectable from "./themes/store.injectable";
import navigateToAddClusterInjectable from "../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable"; import navigateToAddClusterInjectable from "../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable";
import addSyncEntriesInjectable from "./initializers/add-sync-entries.injectable"; import addSyncEntriesInjectable from "./initializers/add-sync-entries.injectable";
import hotbarStoreInjectable from "../common/hotbars/store.injectable"; import hotbarStoreInjectable from "../common/hotbars/store.injectable";

View File

@ -19,8 +19,6 @@ import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-
import catalogEntityRegistryInjectable from "../../api/catalog-entity-registry/catalog-entity-registry.injectable"; import catalogEntityRegistryInjectable from "../../api/catalog-entity-registry/catalog-entity-registry.injectable";
import type { DiRender } from "../test-utils/renderFor"; import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor"; import { renderFor } from "../test-utils/renderFor";
import { ThemeStore } from "../../theme.store";
import { UserStore } from "../../../common/user-store";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
@ -98,8 +96,6 @@ describe("<Catalog />", () => {
await di.runSetups(); await di.runSetups();
mockFs(); mockFs();
UserStore.createInstance();
ThemeStore.createInstance();
CatalogEntityDetailRegistry.createInstance(); CatalogEntityDetailRegistry.createInstance();
render = renderFor(di); render = renderFor(di);
@ -125,8 +121,6 @@ describe("<Catalog />", () => {
}); });
afterEach(() => { afterEach(() => {
UserStore.resetInstance();
ThemeStore.resetInstance();
CatalogEntityDetailRegistry.resetInstance(); CatalogEntityDetailRegistry.resetInstance();
jest.clearAllMocks(); jest.clearAllMocks();
jest.restoreAllMocks(); jest.restoreAllMocks();

View File

@ -11,15 +11,20 @@ import { computed, makeObservable } from "mobx";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { SubHeader } from "../layout/sub-header"; import { SubHeader } from "../layout/sub-header";
import { Table, TableCell, TableHead, TableRow } from "../table"; import { Table, TableCell, TableHead, TableRow } from "../table";
import { nodeStore } from "../+nodes/legacy-store";
import { eventStore } from "../+events/legacy-store";
import { cssNames, prevDefault } from "../../utils"; import { cssNames, prevDefault } from "../../utils";
import type { ItemObject } from "../../../common/item.store"; import type { ItemObject } from "../../../common/item.store";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../themes/store";
import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params"; import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params";
import { apiManager } from "../../../common/k8s-api/api-manager"; import type { ApiManager } from "../../../common/k8s-api/api-manager";
import { KubeObjectAge } from "../kube-object/age"; import { KubeObjectAge } from "../kube-object/age";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import type { NodeStore } from "../+nodes/store";
import type { EventStore } from "../+events/store";
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
import eventStoreInjectable from "../+events/store.injectable";
import nodeStoreInjectable from "../+nodes/store.injectable";
export interface ClusterIssuesProps { export interface ClusterIssuesProps {
className?: string; className?: string;
@ -39,16 +44,23 @@ enum sortBy {
age = "age", age = "age",
} }
interface Dependencies {
themeStore: ThemeStore;
nodeStore: NodeStore;
eventStore: EventStore;
apiManager: ApiManager;
}
@observer @observer
export class ClusterIssues extends React.Component<ClusterIssuesProps> { class NonInjectedClusterIssues extends React.Component<ClusterIssuesProps & Dependencies> {
constructor(props: ClusterIssuesProps) { constructor(props: ClusterIssuesProps & Dependencies) {
super(props); super(props);
makeObservable(this); makeObservable(this);
} }
@computed get warnings(): Warning[] { @computed get warnings(): Warning[] {
return [ return [
...nodeStore.items.flatMap(node => ( ...this.props.nodeStore.items.flatMap(node => (
node.getWarningConditions() node.getWarningConditions()
.map(({ message }) => ({ .map(({ message }) => ({
selfLink: node.selfLink, selfLink: node.selfLink,
@ -60,14 +72,14 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
ageMs: -node.getCreationTimestamp(), ageMs: -node.getCreationTimestamp(),
})) }))
)), )),
...eventStore.getWarnings().map(warning => ({ ...this.props.eventStore.getWarnings().map(warning => ({
getId: () => warning.involvedObject.uid, getId: () => warning.involvedObject.uid,
getName: () => warning.involvedObject.name, getName: () => warning.involvedObject.name,
renderAge: () => <KubeObjectAge key="age" object={warning} />, renderAge: () => <KubeObjectAge key="age" object={warning} />,
ageMs: -warning.getCreationTimestamp(), ageMs: -warning.getCreationTimestamp(),
message: warning.message, message: warning.message,
kind: warning.kind, kind: warning.kind,
selfLink: apiManager.lookupApiLink(warning.involvedObject, warning), selfLink: this.props.apiManager.lookupApiLink(warning.involvedObject, warning),
})), })),
]; ];
} }
@ -108,7 +120,7 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
renderContent() { renderContent() {
const { warnings } = this; const { warnings } = this;
if (!eventStore.isLoaded) { if (!this.props.eventStore.isLoaded) {
return ( return (
<Spinner center/> <Spinner center/>
); );
@ -148,7 +160,7 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }} sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }}
sortSyncWithUrl={false} sortSyncWithUrl={false}
getTableRow={this.getTableRow} getTableRow={this.getTableRow}
className={cssNames("box grow", ThemeStore.getInstance().activeTheme.type)} className={cssNames("box grow", this.props.themeStore.activeTheme.type)}
> >
<TableHead nowrap> <TableHead nowrap>
<TableCell className="message">Message</TableCell> <TableCell className="message">Message</TableCell>
@ -169,3 +181,13 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
); );
} }
} }
export const ClusterIssues = withInjectables<Dependencies, ClusterIssuesProps>(NonInjectedClusterIssues, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
apiManager: di.inject(apiManagerInjectable),
eventStore: di.inject(eventStoreInjectable),
nodeStore: di.inject(nodeStoreInjectable),
}),
});

View File

@ -16,11 +16,12 @@ import type { PieChartData } from "../chart";
import { PieChart } from "../chart"; import { PieChart } from "../chart";
import { ClusterNoMetrics } from "./cluster-no-metrics"; import { ClusterNoMetrics } from "./cluster-no-metrics";
import { bytesToUnits, cssNames } from "../../utils"; import { bytesToUnits, cssNames } from "../../utils";
import { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../themes/store";
import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api"; import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable"; import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
import nodeStoreInjectable from "../+nodes/store.injectable"; import nodeStoreInjectable from "../+nodes/store.injectable";
import themeStoreInjectable from "../../themes/store.injectable";
function createLabels(rawLabelData: [string, number | undefined][]): string[] { function createLabels(rawLabelData: [string, number | undefined][]): string[] {
return rawLabelData.map(([key, value]) => `${key}: ${value?.toFixed(2) || "N/A"}`); return rawLabelData.map(([key, value]) => `${key}: ${value?.toFixed(2) || "N/A"}`);
@ -29,11 +30,13 @@ function createLabels(rawLabelData: [string, number | undefined][]): string[] {
interface Dependencies { interface Dependencies {
clusterOverviewStore: ClusterOverviewStore; clusterOverviewStore: ClusterOverviewStore;
nodeStore: NodeStore; nodeStore: NodeStore;
themeStore: ThemeStore;
} }
const NonInjectedClusterPieCharts = observer(({ const NonInjectedClusterPieCharts = observer(({
clusterOverviewStore, clusterOverviewStore,
nodeStore, nodeStore,
themeStore,
}: Dependencies) => { }: Dependencies) => {
const renderLimitWarning = () => { const renderLimitWarning = () => {
return ( return (
@ -51,7 +54,7 @@ const NonInjectedClusterPieCharts = observer(({
const { podUsage, podAllocatableCapacity, podCapacity } = data; const { podUsage, podAllocatableCapacity, podCapacity } = data;
const cpuLimitsOverload = cpuLimits > cpuAllocatableCapacity; const cpuLimitsOverload = cpuLimits > cpuAllocatableCapacity;
const memoryLimitsOverload = memoryLimits > memoryAllocatableCapacity; const memoryLimitsOverload = memoryLimits > memoryAllocatableCapacity;
const defaultColor = ThemeStore.getInstance().activeTheme.colors.pieChartDefaultColor; const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor;
if (!memoryCapacity || !cpuCapacity || !podCapacity || !memoryAllocatableCapacity || !cpuAllocatableCapacity || !podAllocatableCapacity) return null; if (!memoryCapacity || !cpuCapacity || !podCapacity || !memoryAllocatableCapacity || !cpuAllocatableCapacity || !podAllocatableCapacity) return null;
const cpuData: PieChartData = { const cpuData: PieChartData = {
@ -258,5 +261,6 @@ export const ClusterPieCharts = withInjectables<Dependencies>(NonInjectedCluster
getProps: (di) => ({ getProps: (di) => ({
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
nodeStore: di.inject(nodeStoreInjectable), nodeStore: di.inject(nodeStoreInjectable),
themeStore: di.inject(themeStoreInjectable),
}), }),
}); });

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 "../../../themes/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";
@ -38,6 +38,7 @@ import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-sho
import { KubeObjectAge } from "../../kube-object/age"; import { KubeObjectAge } from "../../kube-object/age";
import type { KubeJsonApiData } from "../../../../common/k8s-api/kube-json-api"; import type { KubeJsonApiData } from "../../../../common/k8s-api/kube-json-api";
import { entries } from "../../../../common/utils/objects"; import { entries } from "../../../../common/utils/objects";
import themeStoreInjectable from "../../../themes/store.injectable";
export interface ReleaseDetailsProps { export interface ReleaseDetailsProps {
hideDetails(): void; hideDetails(): void;
@ -50,6 +51,7 @@ interface Dependencies {
updateRelease: (name: string, namespace: string, payload: HelmReleaseUpdatePayload) => Promise<HelmReleaseUpdateDetails>; updateRelease: (name: string, namespace: string, payload: HelmReleaseUpdatePayload) => Promise<HelmReleaseUpdateDetails>;
createUpgradeChartTab: (release: HelmRelease) => void; createUpgradeChartTab: (release: HelmRelease) => void;
userSuppliedValuesAreShown: { toggle: () => void; value: boolean }; userSuppliedValuesAreShown: { toggle: () => void; value: boolean };
themeStore: ThemeStore;
} }
@observer @observer
@ -252,12 +254,12 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
} }
render() { render() {
const { hideDetails } = this.props; const { hideDetails, themeStore } = this.props;
const release = this.props.release.get(); const release = this.props.release.get();
return ( return (
<Drawer <Drawer
className={cssNames("ReleaseDetails", ThemeStore.getInstance().activeTheme.type)} className={cssNames("ReleaseDetails", themeStore.activeTheme.type)}
usePortal={true} usePortal={true}
open={Boolean(release)} open={Boolean(release)}
title={release ? `Release: ${release.getName()}` : ""} title={release ? `Release: ${release.getName()}` : ""}
@ -285,5 +287,6 @@ export const ReleaseDetails = withInjectables<Dependencies, ReleaseDetailsProps>
userSuppliedValuesAreShown: di.inject(userSuppliedValuesAreShownInjectable), userSuppliedValuesAreShown: di.inject(userSuppliedValuesAreShownInjectable),
updateRelease: di.inject(updateReleaseInjectable), updateRelease: di.inject(updateReleaseInjectable),
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable), createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
themeStore: di.inject(themeStoreInjectable),
}), }),
}); });

View File

@ -12,16 +12,26 @@ import { ResourceMetricsContext } from "../resource-metrics";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { mapValues } from "lodash"; import { mapValues } from "lodash";
import { type MetricsTab, metricTabOptions } from "../chart/options"; import { type MetricsTab, metricTabOptions } from "../chart/options";
import { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../themes/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
export const NodeCharts = observer(() => { export interface NodeChartsProps {}
interface Dependencies {
themeStore: ThemeStore;
}
const NonInjectedNodeCharts = observer(({
themeStore,
}: Dependencies & NodeChartsProps) => {
const { metrics, tab, object } = useContext(ResourceMetricsContext) ?? {}; const { metrics, tab, object } = useContext(ResourceMetricsContext) ?? {};
if (!metrics || !object || !tab) return null; if (!metrics || !object || !tab) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>; if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const id = object.getId(); const id = object.getId();
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors; const { chartCapacityColor } = themeStore.activeTheme.colors;
const { const {
memoryUsage, memoryUsage,
workloadMemoryUsage, workloadMemoryUsage,
@ -148,3 +158,10 @@ export const NodeCharts = observer(() => {
/> />
); );
}); });
export const NodeCharts = withInjectables<Dependencies, NodeChartsProps>(NonInjectedNodeCharts, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
}),
});

View File

@ -7,7 +7,7 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { Select } from "../select"; import { Select } from "../select";
import type { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../themes/store";
import type { UserStore } from "../../../common/user-store"; import type { UserStore } from "../../../common/user-store";
import { Input } from "../input"; import { Input } from "../input";
import { Switch } from "../switch"; import { Switch } from "../switch";
@ -22,8 +22,8 @@ import { withInjectables } from "@ogre-tools/injectable-react";
import appPreferencesInjectable from "./app-preferences/app-preferences.injectable"; import appPreferencesInjectable from "./app-preferences/app-preferences.injectable";
import { Preferences } from "./preferences"; import { Preferences } from "./preferences";
import userStoreInjectable from "../../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import themeStoreInjectable from "../../theme-store.injectable"; import themeStoreInjectable from "../../themes/store.injectable";
import { defaultTheme } from "../../../common/vars"; import { defaultThemeId } from "../../../common/vars";
interface Dependencies { interface Dependencies {
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>; appPreferenceItems: IComputedValue<RegisteredAppPreference[]>;
@ -57,8 +57,6 @@ const extensionInstallRegistryOptions = [
const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, userStore, themeStore }) => { const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, userStore, themeStore }) => {
const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || ""); const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
const extensionSettings = appPreferenceItems.get()
.filter((preference) => preference.showInPreferencesTab === "application");
const themeOptions = [ const themeOptions = [
{ {
value: "system", // TODO: replace with a sentinal value that isn't string (and serialize it differently) value: "system", // TODO: replace with a sentinal value that isn't string (and serialize it differently)
@ -69,6 +67,8 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
label: name, label: name,
})), })),
]; ];
const extensionSettings = appPreferenceItems.get()
.filter((preference) => preference.showInPreferencesTab === "application");
return ( return (
<Preferences data-testid="application-preferences-page"> <Preferences data-testid="application-preferences-page">
@ -79,7 +79,7 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
<Select <Select
options={themeOptions} options={themeOptions}
value={userStore.colorTheme} value={userStore.colorTheme}
onChange={value => userStore.colorTheme = value?.value ?? defaultTheme} onChange={value => userStore.colorTheme = value?.value ?? defaultThemeId}
themeName="lens" themeName="lens"
/> />
</section> </section>

View File

@ -10,11 +10,11 @@ import { SubTitle } from "../layout/sub-title";
import { Input, InputValidators } from "../input"; import { Input, InputValidators } from "../input";
import { Switch } from "../switch"; import { Switch } from "../switch";
import { Select } from "../select"; import { Select } from "../select";
import type { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../themes/store";
import { Preferences } from "./preferences"; import { Preferences } from "./preferences";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import userStoreInjectable from "../../../common/user-store/user-store.injectable"; import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import themeStoreInjectable from "../../theme-store.injectable"; import themeStoreInjectable from "../../themes/store.injectable";
import defaultShellInjectable from "./default-shell.injectable"; import defaultShellInjectable from "./default-shell.injectable";
interface Dependencies { interface Dependencies {

View File

@ -10,16 +10,26 @@ import { BarChart, memoryOptions } from "../chart";
import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics";
import { ResourceMetricsContext } from "../resource-metrics"; import { ResourceMetricsContext } from "../resource-metrics";
import { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../themes/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
export const VolumeClaimDiskChart = observer(() => { export interface VolumeClaimDiskChartProps {}
interface Dependencies {
themeStore: ThemeStore;
}
const NonInjectedVolumeClaimDiskChart = observer(({
themeStore,
}: Dependencies & VolumeClaimDiskChartProps) => {
const { metrics, tab, object } = useContext(ResourceMetricsContext) ?? {}; const { metrics, tab, object } = useContext(ResourceMetricsContext) ?? {};
if (!metrics || !object || !tab) return null; if (!metrics || !object || !tab) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>; if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const id = object.getId(); const id = object.getId();
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors; const { chartCapacityColor } = themeStore.activeTheme.colors;
const { diskUsage, diskCapacity } = metrics; const { diskUsage, diskCapacity } = metrics;
const usage = normalizeMetrics(diskUsage).data.result[0].values; const usage = normalizeMetrics(diskUsage).data.result[0].values;
const capacity = normalizeMetrics(diskCapacity).data.result[0].values; const capacity = normalizeMetrics(diskCapacity).data.result[0].values;
@ -51,3 +61,10 @@ export const VolumeClaimDiskChart = observer(() => {
/> />
); );
}); });
export const VolumeClaimDiskChart = withInjectables<Dependencies, VolumeClaimDiskChartProps>(NonInjectedVolumeClaimDiskChart, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
}),
});

View File

@ -11,14 +11,20 @@ import { observer } from "mobx-react";
import type { DatasetTooltipLabel, PieChartData } from "../chart"; import type { DatasetTooltipLabel, PieChartData } from "../chart";
import { PieChart } from "../chart"; import { PieChart } from "../chart";
import { cssVar, object } from "../../utils"; import { cssVar, object } from "../../utils";
import { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../themes/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
export interface OverviewWorkloadStatusProps { export interface OverviewWorkloadStatusProps {
status: Partial<Record<string, number>>; status: Partial<Record<string, number>>;
} }
interface Dependencies {
themeStore: ThemeStore;
}
@observer @observer
export class OverviewWorkloadStatus extends React.Component<OverviewWorkloadStatusProps> { class NonInjectedOverviewWorkloadStatus extends React.Component<OverviewWorkloadStatusProps & Dependencies> {
private elem: HTMLElement | null = null; private elem: HTMLElement | null = null;
renderChart() { renderChart() {
@ -37,7 +43,7 @@ export class OverviewWorkloadStatus extends React.Component<OverviewWorkloadStat
if (statuses.length === 0) { if (statuses.length === 0) {
chartData.datasets.push({ chartData.datasets.push({
data: [1], data: [1],
backgroundColor: [ThemeStore.getInstance().activeTheme.colors.pieChartDefaultColor], backgroundColor: [this.props.themeStore.activeTheme.colors.pieChartDefaultColor],
label: "Empty", label: "Empty",
}); });
} else { } else {
@ -84,3 +90,10 @@ export class OverviewWorkloadStatus extends React.Component<OverviewWorkloadStat
); );
} }
} }
export const OverviewWorkloadStatus = withInjectables<Dependencies, OverviewWorkloadStatusProps>(NonInjectedOverviewWorkloadStatus, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
}),
});

View File

@ -10,17 +10,27 @@ import { BarChart } from "../chart";
import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics";
import { ResourceMetricsContext } from "../resource-metrics"; import { ResourceMetricsContext } from "../resource-metrics";
import { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../themes/store";
import { mapValues } from "lodash"; import { mapValues } from "lodash";
import { type MetricsTab, metricTabOptions } from "../chart/options"; import { type MetricsTab, metricTabOptions } from "../chart/options";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
export const ContainerCharts = observer(() => { export interface ContainerChartsProps {}
interface Dependencies {
themeStore: ThemeStore;
}
const NonInjectedContainerCharts = observer(({
themeStore,
}: Dependencies & ContainerChartsProps) => {
const { metrics, tab, object } = useContext(ResourceMetricsContext) ?? {}; const { metrics, tab, object } = useContext(ResourceMetricsContext) ?? {};
if (!metrics || !object || !tab) return null; if (!metrics || !object || !tab) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>; if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors; const { chartCapacityColor } = themeStore.activeTheme.colors;
const { const {
cpuUsage, cpuUsage,
cpuRequests, cpuRequests,
@ -113,3 +123,10 @@ export const ContainerCharts = observer(() => {
/> />
); );
}); });
export const ContainerCharts = withInjectables<Dependencies, ContainerChartsProps>(NonInjectedContainerCharts, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
}),
});

View File

@ -13,9 +13,11 @@ import type { ChartProps } from "./chart";
import { Chart, ChartKind } from "./chart"; import { Chart, ChartKind } from "./chart";
import { bytesToUnits, cssNames, isObject } from "../../utils"; import { bytesToUnits, cssNames, isObject } from "../../utils";
import { ZebraStripesPlugin } from "./zebra-stripes.plugin"; import { ZebraStripesPlugin } from "./zebra-stripes.plugin";
import { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../themes/store";
import { NoMetrics } from "../resource-metrics/no-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics";
import assert from "assert"; import assert from "assert";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
export interface BarChartProps extends ChartProps { export interface BarChartProps extends ChartProps {
name?: string; name?: string;
@ -24,7 +26,12 @@ export interface BarChartProps extends ChartProps {
const getBarColor: Scriptable<string> = ({ dataset }) => Color(dataset?.borderColor).alpha(0.2).string(); const getBarColor: Scriptable<string> = ({ dataset }) => Color(dataset?.borderColor).alpha(0.2).string();
export const BarChart = observer(({ interface Dependencies {
themeStore: ThemeStore;
}
const NonInjectedBarChart = observer(({
themeStore,
name, name,
data, data,
className, className,
@ -32,8 +39,8 @@ export const BarChart = observer(({
plugins, plugins,
options: customOptions, options: customOptions,
...settings ...settings
}: BarChartProps) => { }: Dependencies & BarChartProps) => {
const { textColorPrimary, borderFaintColor, chartStripesColor } = ThemeStore.getInstance().activeTheme.colors; const { textColorPrimary, borderFaintColor, chartStripesColor } = themeStore.activeTheme.colors;
const { datasets: rawDatasets = [], ...rest } = data; const { datasets: rawDatasets = [], ...rest } = data;
const datasets = rawDatasets const datasets = rawDatasets
.filter(set => set.data?.length) .filter(set => set.data?.length)
@ -158,6 +165,13 @@ export const BarChart = observer(({
); );
}); });
export const BarChart = withInjectables<Dependencies, BarChartProps>(NonInjectedBarChart, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
}),
});
const tooltipCallbackWith = (precision: number): ChartTooltipCallback["label"] => ( const tooltipCallbackWith = (precision: number): ChartTooltipCallback["label"] => (
({ datasetIndex, index }, { datasets = [] }) => { ({ datasetIndex, index }, { datasets = [] }) => {
if (typeof datasetIndex !== "number" || typeof index !== "number") { if (typeof datasetIndex !== "number" || typeof index !== "number") {

View File

@ -11,7 +11,9 @@ import ChartJS from "chart.js";
import type { ChartProps } from "./chart"; import type { ChartProps } from "./chart";
import { Chart } from "./chart"; import { Chart } from "./chart";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../themes/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
export interface PieChartProps extends ChartProps { export interface PieChartProps extends ChartProps {
} }
@ -41,9 +43,19 @@ function getCutout(length: number | undefined): number {
} }
} }
export const PieChart = observer((props: PieChartProps) => { interface Dependencies {
const { data, className, options, ...chartProps } = props; themeStore: ThemeStore;
const { contentColor } = ThemeStore.getInstance().activeTheme.colors; }
const NonInjectedPieChart = observer(({
themeStore,
data,
className,
options,
showChart,
...chartProps
}: Dependencies & PieChartProps) => {
const { contentColor } = themeStore.activeTheme.colors;
const opts: ChartOptions = { const opts: ChartOptions = {
maintainAspectRatio: false, maintainAspectRatio: false,
tooltips: { tooltips: {
@ -98,12 +110,20 @@ export const PieChart = observer((props: PieChartProps) => {
<Chart <Chart
className={cssNames("PieChart flex column align-center", className)} className={cssNames("PieChart flex column align-center", className)}
data={data} data={data}
options={props.showChart ? {} : opts} options={showChart ? {} : opts}
showChart={showChart}
{...chartProps} {...chartProps}
/> />
); );
}); });
export const PieChart = withInjectables<Dependencies, PieChartProps>(NonInjectedPieChart, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
}),
});
ChartJS.Tooltip.positioners.cursor = function (elements: any, position: { x: number; y: number }) { ChartJS.Tooltip.positioners.cursor = function (elements: any, position: { x: number; y: number }) {
return position; return position;
}; };

View File

@ -11,8 +11,6 @@ import { DockTabs } from "../dock-tabs";
import type { DockStore, DockTab } from "../dock/store"; import type { DockStore, DockTab } from "../dock/store";
import { TabKind } from "../dock/store"; import { TabKind } from "../dock/store";
import { noop } from "../../../utils"; import { noop } from "../../../utils";
import { ThemeStore } from "../../../theme.store";
import { UserStore } from "../../../../common/user-store";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import dockStoreInjectable from "../dock/store.injectable"; import dockStoreInjectable from "../dock/store.injectable";
import type { DiRender } from "../../test-utils/renderFor"; import type { DiRender } from "../../test-utils/renderFor";
@ -92,16 +90,11 @@ describe("<DockTabs />", () => {
dockStore = di.inject(dockStoreInjectable); dockStore = di.inject(dockStoreInjectable);
UserStore.createInstance();
ThemeStore.createInstance();
await dockStore.whenReady; await dockStore.whenReady;
dockStore.tabs = initialTabs; dockStore.tabs = initialTabs;
}); });
afterEach(() => { afterEach(() => {
ThemeStore.resetInstance();
UserStore.resetInstance();
// TODO: A unit test may not cause side effects. Here accessing file system is a side effect. // TODO: A unit test may not cause side effects. Here accessing file system is a side effect.
fse.remove("some-test-suite-specific-directory-for-user-data"); fse.remove("some-test-suite-specific-directory-for-user-data");
}); });

View File

@ -8,8 +8,6 @@ import "@testing-library/jest-dom/extend-expect";
import * as selectEvent from "react-select-event"; import * as selectEvent from "react-select-event";
import { LogResourceSelector } from "../resource-selector"; import { LogResourceSelector } from "../resource-selector";
import { dockerPod, deploymentPod1, deploymentPod2 } from "./pod.mock"; import { dockerPod, deploymentPod1, deploymentPod2 } from "./pod.mock";
import { ThemeStore } from "../../../../theme.store";
import { UserStore } from "../../../../../common/user-store";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
import type { DiRender } from "../../../test-utils/renderFor"; import type { DiRender } from "../../../test-utils/renderFor";
@ -144,14 +142,9 @@ describe("<LogResourceSelector />", () => {
mockFs({ mockFs({
"tmp": {}, "tmp": {},
}); });
UserStore.createInstance();
ThemeStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
UserStore.resetInstance();
ThemeStore.resetInstance();
mockFs.restore(); mockFs.restore();
}); });

View File

@ -10,6 +10,7 @@ import type { TerminalApi } from "../../../api/terminal-api";
import terminalSpawningPoolInjectable from "./terminal-spawning-pool.injectable"; import terminalSpawningPoolInjectable from "./terminal-spawning-pool.injectable";
import terminalConfigInjectable from "../../../../common/user-store/terminal-config.injectable"; import terminalConfigInjectable from "../../../../common/user-store/terminal-config.injectable";
import terminalCopyOnSelectInjectable from "../../../../common/user-store/terminal-copy-on-select.injectable"; import terminalCopyOnSelectInjectable from "../../../../common/user-store/terminal-copy-on-select.injectable";
import themeStoreInjectable from "../../../themes/store.injectable";
export type CreateTerminal = (tabId: TabId, api: TerminalApi) => Terminal; export type CreateTerminal = (tabId: TabId, api: TerminalApi) => Terminal;
@ -20,6 +21,7 @@ const createTerminalInjectable = getInjectable({
spawningPool: di.inject(terminalSpawningPoolInjectable), spawningPool: di.inject(terminalSpawningPoolInjectable),
terminalConfig: di.inject(terminalConfigInjectable), terminalConfig: di.inject(terminalConfigInjectable),
terminalCopyOnSelect: di.inject(terminalCopyOnSelectInjectable), terminalCopyOnSelect: di.inject(terminalCopyOnSelectInjectable),
themeStore: di.inject(themeStoreInjectable),
}; };
return (tabId, api) => new Terminal(dependencies, { tabId, api }); return (tabId, api) => new Terminal(dependencies, { tabId, api });

View File

@ -11,7 +11,7 @@ import { FitAddon } from "xterm-addon-fit";
import type { TabId } from "../dock/store"; import type { TabId } from "../dock/store";
import type { TerminalApi } from "../../../api/terminal-api"; import type { TerminalApi } from "../../../api/terminal-api";
import { TerminalChannels } from "../../../api/terminal-api"; import { TerminalChannels } from "../../../api/terminal-api";
import { ThemeStore } from "../../../theme.store"; import type { ThemeStore } from "../../../themes/store";
import { disposer } from "../../../utils"; import { disposer } from "../../../utils";
import { isMac } from "../../../../common/vars"; import { isMac } from "../../../../common/vars";
import { once } from "lodash"; import { once } from "lodash";
@ -24,6 +24,7 @@ export interface TerminalDependencies {
readonly spawningPool: HTMLElement; readonly spawningPool: HTMLElement;
readonly terminalConfig: IComputedValue<TerminalConfig>; readonly terminalConfig: IComputedValue<TerminalConfig>;
readonly terminalCopyOnSelect: IComputedValue<boolean>; readonly terminalCopyOnSelect: IComputedValue<boolean>;
readonly themeStore: ThemeStore;
} }
export interface TerminalArguments { export interface TerminalArguments {
@ -94,7 +95,7 @@ export class Terminal {
window.addEventListener("resize", this.onResize); window.addEventListener("resize", this.onResize);
this.disposer.push( this.disposer.push(
reaction(() => ThemeStore.getInstance().xtermColors, colors => { reaction(() => this.dependencies.themeStore.xtermColors, colors => {
this.xterm?.setOption("theme", colors); this.xterm?.setOption("theme", colors);
}, { }, {
fireImmediately: true, fireImmediately: true,

View File

@ -10,12 +10,13 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { cssNames } from "../../../utils"; import { cssNames } from "../../../utils";
import type { Terminal } from "./terminal"; import type { Terminal } from "./terminal";
import type { TerminalStore } from "./store"; import type { TerminalStore } from "./store";
import { ThemeStore } from "../../../theme.store"; import type { ThemeStore } from "../../../themes/store";
import type { DockTab, DockStore } from "../dock/store"; import type { DockTab, DockStore } from "../dock/store";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "../dock/store.injectable"; import dockStoreInjectable from "../dock/store.injectable";
import terminalStoreInjectable from "./store.injectable"; import terminalStoreInjectable from "./store.injectable";
import assert from "assert"; import assert from "assert";
import themeStoreInjectable from "../../../themes/store.injectable";
export interface TerminalWindowProps { export interface TerminalWindowProps {
tab: DockTab; tab: DockTab;
@ -24,6 +25,7 @@ export interface TerminalWindowProps {
interface Dependencies { interface Dependencies {
dockStore: DockStore; dockStore: DockStore;
terminalStore: TerminalStore; terminalStore: TerminalStore;
themeStore: ThemeStore;
} }
@observer @observer
@ -66,7 +68,7 @@ class NonInjectedTerminalWindow extends React.Component<TerminalWindowProps & De
render() { render() {
return ( return (
<div <div
className={cssNames("TerminalWindow", ThemeStore.getInstance().activeTheme.type)} className={cssNames("TerminalWindow", this.props.themeStore.activeTheme.type)}
ref={elem => this.elem = elem} ref={elem => this.elem = elem}
/> />
); );
@ -75,9 +77,10 @@ class NonInjectedTerminalWindow extends React.Component<TerminalWindowProps & De
export const TerminalWindow = withInjectables<Dependencies, TerminalWindowProps>(NonInjectedTerminalWindow, { export const TerminalWindow = withInjectables<Dependencies, TerminalWindowProps>(NonInjectedTerminalWindow, {
getProps: (di, props) => ({ getProps: (di, props) => ({
...props,
dockStore: di.inject(dockStoreInjectable), dockStore: di.inject(dockStoreInjectable),
terminalStore: di.inject(terminalStoreInjectable), terminalStore: di.inject(terminalStoreInjectable),
...props, themeStore: di.inject(themeStoreInjectable),
}), }),
}); });

View File

@ -12,9 +12,7 @@ import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor"; import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor"; import { renderFor } from "../../test-utils/renderFor";
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable"; import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
import { ThemeStore } from "../../../theme.store";
import { ConfirmDialog } from "../../confirm-dialog"; import { ConfirmDialog } from "../../confirm-dialog";
import { UserStore } from "../../../../common/user-store";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../../../../common/get-configuration-file-model/get-configuration-file-model.injectable"; import getConfigurationFileModelInjectable from "../../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
@ -54,15 +52,10 @@ describe("<HotbarRemoveCommand />", () => {
di.permitSideEffects(appVersionInjectable); di.permitSideEffects(appVersionInjectable);
render = renderFor(di); render = renderFor(di);
UserStore.createInstance();
ThemeStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
ThemeStore.resetInstance();
UserStore.resetInstance();
}); });
it("renders w/o errors", async () => { it("renders w/o errors", async () => {

View File

@ -22,39 +22,41 @@ import { Spinner } from "../spinner";
import type { ItemObject } from "../../../common/item.store"; import type { ItemObject } 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 "../../themes/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 { UserStore } from "../../../common/user-store";
import type { ItemListStore } from "./list-layout"; import type { ItemListStore } from "./list-layout";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
export interface ItemListLayoutContentProps<I extends ItemObject, PreLoadStores extends boolean> { export interface ItemListLayoutContentProps<Item extends ItemObject, PreLoadStores extends boolean> {
getFilters: () => Filter[]; getFilters: () => Filter[];
tableId?: string; tableId?: string;
className: IClassName; className: IClassName;
getItems: () => I[]; getItems: () => Item[];
store: ItemListStore<I, PreLoadStores>; store: ItemListStore<Item, PreLoadStores>;
getIsReady: () => boolean; // show loading indicator while not ready getIsReady: () => boolean; // show loading indicator while not ready
isSelectable?: boolean; // show checkbox in rows for selecting items isSelectable?: boolean; // show checkbox in rows for selecting items
isConfigurable?: boolean; isConfigurable?: boolean;
copyClassNameFromHeadCells?: boolean; copyClassNameFromHeadCells?: boolean;
sortingCallbacks?: TableSortCallbacks<I>; sortingCallbacks?: TableSortCallbacks<Item>;
tableProps?: Partial<TableProps<I>>; // low-level table configuration tableProps?: Partial<TableProps<Item>>; // low-level table configuration
renderTableHeader?: (TableCellProps | undefined | null)[]; renderTableHeader?: (TableCellProps | undefined | null)[];
renderTableContents: (item: I) => (ReactNode | TableCellProps)[]; renderTableContents: (item: Item) => (ReactNode | TableCellProps)[];
renderItemMenu?: (item: I, store: ItemListStore<I, PreLoadStores>) => ReactNode; renderItemMenu?: (item: Item, store: ItemListStore<Item, PreLoadStores>) => ReactNode;
customizeTableRowProps?: (item: I) => Partial<TableRowProps<I>>; customizeTableRowProps?: (item: Item) => Partial<TableRowProps<Item>>;
addRemoveButtons?: Partial<AddRemoveButtonsProps>; addRemoveButtons?: Partial<AddRemoveButtonsProps>;
virtual?: boolean; virtual?: boolean;
// item details view // item details view
hasDetailsView?: boolean; hasDetailsView?: boolean;
detailsItem?: I; detailsItem?: Item;
onDetails?: (item: I) => void; onDetails?: (item: Item) => void;
// other // other
customizeRemoveDialog?: (selectedItems: I[]) => Partial<ConfirmDialogParams>; customizeRemoveDialog?: (selectedItems: Item[]) => Partial<ConfirmDialogParams>;
/** /**
* Message to display when a store failed to load * Message to display when a store failed to load
@ -64,9 +66,13 @@ export interface ItemListLayoutContentProps<I extends ItemObject, PreLoadStores
failedToLoadMessage?: React.ReactNode; failedToLoadMessage?: React.ReactNode;
} }
interface Dependencies {
themeStore: ThemeStore;
}
@observer @observer
export class ItemListLayoutContent<I extends ItemObject, PreLoadStores extends boolean> extends React.Component<ItemListLayoutContentProps<I, PreLoadStores>> { class NonInjectedItemListLayoutContent<Item extends ItemObject, PreLoadStores extends boolean> extends React.Component<ItemListLayoutContentProps<Item, PreLoadStores> & Dependencies> {
constructor(props: ItemListLayoutContentProps<I, PreLoadStores>) { constructor(props: ItemListLayoutContentProps<Item, PreLoadStores> & Dependencies) {
super(props); super(props);
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
@ -76,11 +82,11 @@ export class ItemListLayoutContent<I extends ItemObject, PreLoadStores extends b
return this.props.store.failedLoading; return this.props.store.failedLoading;
} }
renderRow(item: I) { renderRow(item: Item) {
return this.getTableRow(item); return this.getTableRow(item);
} }
getTableRow(item: I) { getTableRow(item: Item) {
const { const {
isSelectable, renderTableHeader, renderTableContents, renderItemMenu, isSelectable, renderTableHeader, renderTableContents, renderItemMenu,
store, hasDetailsView, onDetails, store, hasDetailsView, onDetails,
@ -150,7 +156,7 @@ export class ItemListLayoutContent<I extends ItemObject, PreLoadStores extends b
); );
} }
removeItemsDialog(selectedItems: I[]) { removeItemsDialog(selectedItems: Item[]) {
const { customizeRemoveDialog, store } = this.props; const { customizeRemoveDialog, store } = this.props;
const visibleMaxNamesCount = 5; const visibleMaxNamesCount = 5;
const selectedNames = selectedItems.map(ns => ns.getName()).slice(0, visibleMaxNamesCount).join(", "); const selectedNames = selectedItems.map(ns => ns.getName()).slice(0, visibleMaxNamesCount).join(", ");
@ -275,10 +281,10 @@ export class ItemListLayoutContent<I extends ItemObject, PreLoadStores extends b
render() { render() {
const { const {
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks,
detailsItem, className, tableProps = {}, tableId, getItems, detailsItem, className, tableProps = {}, tableId, getItems, themeStore,
} = 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", themeStore.activeTheme.type);
const items = getItems(); const items = getItems();
const selectedItems = store.pickOnlySelected(items); const selectedItems = store.pickOnlySelected(items);
@ -356,3 +362,10 @@ export class ItemListLayoutContent<I extends ItemObject, PreLoadStores extends b
); );
} }
} }
export const ItemListLayoutContent = withInjectables<Dependencies, ItemListLayoutContentProps<ItemObject, boolean>>(NonInjectedItemListLayoutContent, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
}),
}) as <Item extends ItemObject, PreLoadStores extends boolean>(props: ItemListLayoutContentProps<Item, PreLoadStores>) => React.ReactElement;

View File

@ -13,7 +13,9 @@ import { type MonacoValidator, monacoValidators } from "./monaco-validators";
import { debounce, merge } from "lodash"; import { debounce, merge } from "lodash";
import { autoBind, cssNames, disposer } from "../../utils"; import { autoBind, cssNames, disposer } from "../../utils";
import { UserStore } from "../../../common/user-store"; import { UserStore } from "../../../common/user-store";
import { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../themes/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import logger from "../../../main/logger"; import logger from "../../../main/logger";
export type MonacoEditorId = string; export type MonacoEditorId = string;
@ -35,23 +37,22 @@ export interface MonacoEditorProps {
onModelChange?(model: editor.ITextModel, prev?: editor.ITextModel): void; onModelChange?(model: editor.ITextModel, prev?: editor.ITextModel): void;
} }
export const defaultEditorProps: Partial<MonacoEditorProps> = { interface Dependencies {
language: "yaml", themeStore: ThemeStore;
get theme(): MonacoTheme { }
// theme for monaco-editor defined in `src/renderer/themes/lens-*.json`
return ThemeStore.getInstance().activeTheme.monacoTheme;
},
};
@observer export function createMonacoUri(id: MonacoEditorId): Uri {
export class MonacoEditor extends React.Component<MonacoEditorProps> {
static readonly defaultProps = defaultEditorProps as object;
static readonly viewStates = new WeakMap<Uri, editor.ICodeEditorViewState>();
static createUri(id: MonacoEditorId): Uri {
return Uri.file(`/monaco-editor/${id}`); return Uri.file(`/monaco-editor/${id}`);
} }
const monacoViewStates = new WeakMap<Uri, editor.ICodeEditorViewState>();
@observer
class NonInjectedMonacoEditor extends React.Component<MonacoEditorProps & Dependencies> {
static defaultProps = {
language: "yaml" as const,
};
private staticId = `editor-id#${Math.round(1e7 * Math.random())}`; private staticId = `editor-id#${Math.round(1e7 * Math.random())}`;
private dispose = disposer(); private dispose = disposer();
@ -60,7 +61,7 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
@observable readonly dimensions: { width?: number; height?: number } = {}; @observable readonly dimensions: { width?: number; height?: number } = {};
@observable unmounting = false; @observable unmounting = false;
constructor(props: MonacoEditorProps) { constructor(props: MonacoEditorProps & Dependencies) {
super(props); super(props);
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
@ -70,8 +71,12 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
return this.props.id ?? this.staticId; return this.props.id ?? this.staticId;
} }
@computed get theme() {
return this.props.theme ?? this.props.themeStore.activeTheme.monacoTheme;
}
@computed get model(): editor.ITextModel { @computed get model(): editor.ITextModel {
const uri = MonacoEditor.createUri(this.id); const uri = createMonacoUri(this.id);
const model = editor.getModel(uri); const model = editor.getModel(uri);
if (model) { if (model) {
@ -141,12 +146,12 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
const viewState = this.editor?.saveViewState(); const viewState = this.editor?.saveViewState();
if (viewState) { if (viewState) {
MonacoEditor.viewStates.set(model.uri, viewState); monacoViewStates.set(model.uri, viewState);
} }
} }
protected restoreViewState(model: editor.ITextModel) { protected restoreViewState(model: editor.ITextModel) {
const viewState = MonacoEditor.viewStates.get(model.uri); const viewState = monacoViewStates.get(model.uri);
if (viewState) { if (viewState) {
this.editor?.restoreViewState(viewState); this.editor?.restoreViewState(viewState);
@ -177,7 +182,8 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
if (!this.containerElem || this.editor || this.unmounting) { if (!this.containerElem || this.editor || this.unmounting) {
return; return;
} }
const { language, theme, readOnly, value: defaultValue } = this.props; const { language, readOnly, value: defaultValue } = this.props;
const { theme } = this;
this.editor = editor.create(this.containerElem, { this.editor = editor.create(this.containerElem, {
model: this.model, model: this.model,
@ -214,7 +220,7 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
this.dispose.push( this.dispose.push(
reaction(() => this.model, this.onModelChange), reaction(() => this.model, this.onModelChange),
reaction(() => this.props.theme, theme => { reaction(() => this.theme, theme => {
if (theme) { if (theme) {
editor.setTheme(theme); editor.setTheme(theme);
} }
@ -283,3 +289,10 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
); );
} }
} }
export const MonacoEditor = withInjectables<Dependencies, MonacoEditorProps>(NonInjectedMonacoEditor, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
}),
});

View File

@ -6,8 +6,6 @@ import React from "react";
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import type { SelectOption } from "./select"; import type { SelectOption } from "./select";
import { Select } from "./select"; import { Select } from "./select";
import { UserStore } from "../../../common/user-store";
import { ThemeStore } from "../../theme.store";
import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import type { DiRender } from "../test-utils/renderFor"; import type { DiRender } from "../test-utils/renderFor";
@ -42,15 +40,9 @@ describe("<Select />", () => {
di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable); di.permitSideEffects(appVersionInjectable);
UserStore.createInstance();
ThemeStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
ThemeStore.resetInstance();
UserStore.resetInstance();
mockFs.restore(); mockFs.restore();
}); });

View File

@ -12,9 +12,11 @@ import { action, computed, makeObservable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import ReactSelect, { components, createFilter } from "react-select"; import ReactSelect, { components, createFilter } from "react-select";
import type { Props as ReactSelectProps, GroupBase, MultiValue, OptionsOrGroups, PropsValue, SingleValue } from "react-select"; import type { Props as ReactSelectProps, GroupBase, MultiValue, OptionsOrGroups, PropsValue, SingleValue } from "react-select";
import { ThemeStore } from "../../theme.store"; import type { ThemeStore } from "../../themes/store";
import { autoBind, cssNames } from "../../utils"; import { autoBind, cssNames } from "../../utils";
import type { SetRequired } from "type-fest"; import type { SetRequired } from "type-fest";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
const { Menu } = components; const { Menu } = components;
@ -60,26 +62,30 @@ const defaultFilter = createFilter({
}, },
}); });
interface Dependencies {
themeStore: ThemeStore;
}
@observer @observer
export class Select< class NonInjectedSelect<
Value, Value,
Option extends SelectOption<Value>, Option extends SelectOption<Value>,
IsMulti extends boolean = false, IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>, Group extends GroupBase<Option> = GroupBase<Option>,
> extends React.Component<SelectProps<Value, Option, IsMulti, Group>> { > extends React.Component<SelectProps<Value, Option, IsMulti, Group> & Dependencies> {
static defaultProps = { static defaultProps = {
menuPortalTarget: document.body, menuPortalTarget: document.body,
menuPlacement: "auto", menuPlacement: "auto" as const,
}; };
constructor(props: SelectProps<Value, Option, IsMulti, Group>) { constructor(props: SelectProps<Value, Option, IsMulti, Group> & 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}`;
} }
@ -183,3 +189,15 @@ export class Select<
); );
} }
} }
export const Select = withInjectables<Dependencies, SelectProps<unknown, SelectOption<unknown>, boolean>>(NonInjectedSelect, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
}),
}) as <
Value,
Option extends SelectOption<Value>,
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>,
>(props: SelectProps<Value, Option, IsMulti, Group>) => React.ReactElement;

View File

@ -1,20 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { ThemeStore } from "./theme.store";
const themeStoreInjectable = getInjectable({
id: "theme-store",
instantiate: () => {
ThemeStore.resetInstance();
return ThemeStore.createInstance();
},
causesSideEffects: true,
});
export default themeStoreInjectable;

View File

@ -3,7 +3,7 @@
* 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 type { Theme } from "../theme.store"; import type { Theme } from "./store";
const lensDarkTheme: Theme = { const lensDarkTheme: Theme = {
"name": "Dark", "name": "Dark",

View File

@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* 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 type { Theme } from "../theme.store"; import type { Theme } from "./store";
const lensLightTheme: Theme = { const lensLightTheme: Theme = {
"name": "Light", "name": "Light",

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import userStoreInjectable from "../../common/user-store/user-store.injectable";
import ipcRendererInjectable from "../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable";
import { ThemeStore } from "./store";
const themeStoreInjectable = getInjectable({
id: "theme-store",
instantiate: (di) => new ThemeStore({
ipcRenderer: di.inject(ipcRendererInjectable),
userStore: di.inject(userStoreInjectable),
}),
causesSideEffects: true,
});
export default themeStoreInjectable;

View File

@ -4,17 +4,17 @@
*/ */
import { comparer, computed, makeObservable, observable, reaction } from "mobx"; import { comparer, computed, makeObservable, observable, reaction } from "mobx";
import { autoBind, Singleton } from "./utils"; import { autoBind } from "../utils";
import { UserStore } from "../common/user-store"; import logger from "../../main/logger";
import logger from "../main/logger"; import lensDarkTheme from "./lens-dark";
import lensDarkTheme from "./themes/lens-dark"; import lensLightTheme from "./lens-light";
import lensLightTheme from "./themes/lens-light"; import type { MonacoTheme } from "../components/monaco-editor";
import type { MonacoTheme } from "./components/monaco-editor"; import { defaultThemeId } from "../../common/vars";
import { defaultTheme } from "../common/vars";
import { camelCase } from "lodash"; import { camelCase } from "lodash";
import { ipcRenderer } from "electron"; import type { IpcRenderer } from "electron";
import { getNativeThemeChannel, setNativeThemeChannel } from "../common/ipc/native-theme"; import { getNativeThemeChannel, setNativeThemeChannel } from "../../common/ipc/native-theme";
import type { ReadonlyDeep } from "type-fest/source/readonly-deep"; import type { ReadonlyDeep } from "type-fest/source/readonly-deep";
import assert from "assert";
export type ThemeId = string; export type ThemeId = string;
@ -27,7 +27,16 @@ export interface Theme {
monacoTheme: MonacoTheme; monacoTheme: MonacoTheme;
} }
export class ThemeStore extends Singleton { interface Dependencies {
readonly userStore: {
colorTheme: string;
terminalTheme: ThemeId;
resetTheme(): void;
};
readonly ipcRenderer: IpcRenderer;
}
export class ThemeStore {
private terminalColorPrefix = "terminal"; private terminalColorPrefix = "terminal";
#themes = observable.map<ThemeId, Theme>({ #themes = observable.map<ThemeId, Theme>({
@ -38,18 +47,19 @@ export class ThemeStore extends Singleton {
@observable osNativeTheme: "dark" | "light" | undefined; @observable osNativeTheme: "dark" | "light" | undefined;
@computed get activeThemeId(): ThemeId { @computed get activeThemeId(): ThemeId {
return UserStore.getInstance().colorTheme; return this.dependencies.userStore.colorTheme;
} }
@computed get terminalThemeId(): ThemeId | undefined { @computed get terminalThemeId(): ThemeId {
return UserStore.getInstance().terminalTheme; return this.dependencies.userStore.terminalTheme;
} }
private readonly defaultTheme: Theme;
@computed get activeTheme(): Theme { @computed get activeTheme(): Theme {
return this.systemTheme return this.systemTheme
?? this.#themes.get(this.activeThemeId) ?? this.#themes.get(this.activeThemeId)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion ?? this.defaultTheme;
?? this.#themes.get(defaultTheme)!;
} }
@computed get terminalColors(): [string, string][] { @computed get terminalColors(): [string, string][] {
@ -85,12 +95,16 @@ export class ThemeStore extends Singleton {
return null; return null;
} }
constructor() { constructor(protected readonly dependencies: Dependencies) {
super();
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
this.init(); this.init();
const defaultTheme = this.#themes.get(defaultThemeId);
assert(defaultTheme, `${defaultThemeId} is invalid as there is no corresponding theme`);
this.defaultTheme = defaultTheme;
} }
async init() { async init() {
@ -106,7 +120,7 @@ export class ThemeStore extends Singleton {
this.applyActiveTheme(); this.applyActiveTheme();
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
UserStore.getInstance().resetTheme(); this.dependencies.userStore.resetTheme();
} }
}, { }, {
fireImmediately: true, fireImmediately: true,
@ -115,14 +129,14 @@ export class ThemeStore extends Singleton {
} }
bindNativeThemeUpdateEvent() { bindNativeThemeUpdateEvent() {
ipcRenderer.on(setNativeThemeChannel, (event, theme: "dark" | "light") => { this.dependencies.ipcRenderer.on(setNativeThemeChannel, (event, theme: "dark" | "light") => {
this.osNativeTheme = theme; this.osNativeTheme = theme;
this.applyActiveTheme(); this.applyActiveTheme();
}); });
} }
async setNativeTheme() { async setNativeTheme() {
const theme: "dark" | "light" = await ipcRenderer.invoke(getNativeThemeChannel); const theme: "dark" | "light" = await this.dependencies.ipcRenderer.invoke(getNativeThemeChannel);
this.osNativeTheme = theme; this.osNativeTheme = theme;
} }