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

@ -105,7 +105,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="SubTitle"
>
Theme
</div>
<div
class="Select theme-lens css-b62m3t-container"
@ -190,7 +190,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="SubTitle"
>
Extension Install Registry
</div>
<div
class="Select theme-lens css-b62m3t-container"
@ -270,16 +270,11 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="mt-4 mb-5 leading-relaxed"
>
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>
.npmrc
</b>
 file or in the input below.
file or in the input below.
</p>
<div
class="Input theme round black disabled invalid"
@ -309,7 +304,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="SubTitle"
>
Start-up
</div>
<label
class="Switch"
@ -330,7 +325,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="SubTitle"
>
Update Channel
</div>
<div
class="Select theme-lens css-b62m3t-container"
@ -415,7 +410,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="SubTitle"
>
Locale Timezone
</div>
<div
class="Select theme-lens css-b62m3t-container"
@ -634,7 +629,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="SubTitle"
>
Terminal Shell Path
</div>
<div
class="Input theme round black"
@ -662,7 +657,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="SubTitle"
>
Terminal copy & paste
</div>
<label
class="Switch"
@ -682,7 +677,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="SubTitle"
>
Terminal theme
</div>
<div
class="Select theme-lens css-b62m3t-container"
@ -764,7 +759,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="SubTitle"
>
Font size
</div>
<div
class="Input theme round black"
@ -791,7 +786,7 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="SubTitle"
>
Font family
</div>
<div
class="Input theme round black"

View File

@ -12,8 +12,8 @@ import { routeInjectionToken } from "../../common/front-end-routing/route-inject
import { computed } from "mobx";
import type { UserStore } from "../../common/user-store";
import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import type { ThemeStore } from "../../renderer/themes/store";
import themeStoreInjectable from "../../renderer/themes/store.injectable";
import { preferenceNavigationItemInjectionToken } from "../../renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../renderer/routes/route-is-active.injectable";
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 userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import themeStoreInjectable from "../../renderer/themes/store.injectable";
import type { ThemeStore } from "../../renderer/themes/store";
import navigateToProxyPreferencesInjectable
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 userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import themeStoreInjectable from "../../renderer/themes/store.injectable";
import type { ThemeStore } from "../../renderer/themes/store";
describe("preferences - navigation to editor preferences", () => {
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 userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import type { LensRendererExtension } from "../../extensions/lens-renderer-extension";
import themeStoreInjectable from "../../renderer/themes/store.injectable";
import type { ThemeStore } from "../../renderer/themes/store";
import { LensRendererExtension } from "../../extensions/lens-renderer-extension";
import React from "react";
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> = {
id: "some-test-extension-id",
appPreferences: [
const extensionStubWithExtensionSpecificPreferenceItems = new (class extends LensRendererExtension{
appPreferences = [
{
title: "Some preference item",
id: "some-preference-item-id",
@ -131,6 +129,17 @@ const extensionStubWithExtensionSpecificPreferenceItems: Partial<LensRendererExt
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 userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import themeStoreInjectable from "../../renderer/themes/store.injectable";
import type { ThemeStore } from "../../renderer/themes/store";
import { observable } from "mobx";
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 userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { UserStore } from "../../common/user-store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import themeStoreInjectable from "../../renderer/themes/store.injectable";
import type { ThemeStore } from "../../renderer/themes/store";
describe("preferences - navigation to proxy preferences", () => {
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 type { UserStore } from "../../common/user-store";
import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import type { ThemeStore } from "../../renderer/themes/store";
import themeStoreInjectable from "../../renderer/themes/store.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";

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 userStoreInjectable from "../../common/user-store/user-store.injectable";
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 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", () => {
let applicationBuilder: ApplicationBuilder;
@ -26,12 +26,12 @@ describe("preferences - navigation to terminal preferences", () => {
} as unknown as UserStore;
rendererDi.override(userStoreInjectable, () => userStoreStub);
rendererDi.override(defaultShellInjectable, () => "some-default-shell");
const themeStoreStub = ({ themeOptions: [] }) as unknown as ThemeStore;
rendererDi.override(themeStoreInjectable, () => themeStoreStub);
rendererDi.permitSideEffects(themeStoreInjectable);
rendererDi.override(ipcRendererInjectable, () => ({
on: jest.fn(),
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 type { UserStore } from "../../common/user-store";
import userStoreInjectable from "../../common/user-store/user-store.injectable";
import type { ThemeStore } from "../../renderer/theme.store";
import themeStoreInjectable from "../../renderer/theme-store.injectable";
import type { ThemeStore } from "../../renderer/themes/store";
import themeStoreInjectable from "../../renderer/themes/store.injectable";
describe("preferences - navigation using application menu", () => {
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 directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import type { ClusterStoreModel } from "../cluster-store/cluster-store";
import { defaultTheme } from "../vars";
import { defaultThemeId } from "../vars";
import writeFileInjectable from "../fs/write-file.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable
@ -80,7 +80,7 @@ describe("user store tests", () => {
userStore.httpsProxy = "abcd://defg";
expect(userStore.httpsProxy).toBe("abcd://defg");
expect(userStore.colorTheme).toBe(defaultTheme);
expect(userStore.colorTheme).toBe(defaultThemeId);
userStore.colorTheme = "light";
expect(userStore.colorTheme).toBe("light");
@ -89,7 +89,7 @@ describe("user store tests", () => {
it("correctly resets theme to default value", async () => {
userStore.colorTheme = "some other theme";
userStore.resetTheme();
expect(userStore.colorTheme).toBe(defaultTheme);
expect(userStore.colorTheme).toBe(defaultThemeId);
});
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 merge from "lodash/merge";
import { SemVer } from "semver";
import { defaultTheme, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars";
import { defaultThemeId, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars";
import type { ObservableMap } from "mobx";
import { observable } from "mobx";
import { readonly } from "../utils/readonly";
@ -77,10 +77,10 @@ const shell: PreferenceDescription<string | undefined> = {
const colorTheme: PreferenceDescription<string> = {
fromStore(val) {
return val || defaultTheme;
return val || defaultThemeId;
},
toStore(val) {
if (!val || val === defaultTheme) {
if (!val || val === defaultThemeId) {
return undefined;
}

View File

@ -7,6 +7,7 @@
import path from "path";
import { SemVer } from "semver";
import packageInfo from "../../package.json";
import type { ThemeId } from "../renderer/themes/store";
import { lazyInitialized } from "./utils/lazy-initialized";
export const isMac = process.platform === "darwin";
@ -25,7 +26,7 @@ export const isIntegrationTesting = process.argv.includes(integrationTestingArg)
export const productName = packageInfo.productName;
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
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 defaultTerminalFontFamily = "RobotoMono";
export const defaultEditorFontFamily = "RobotoMono";

View File

@ -3,8 +3,11 @@
* 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() {
return ThemeStore.getInstance().activeTheme;
return themeStore.activeTheme;
}

View File

@ -5,12 +5,16 @@
import { getInjectable } from "@ogre-tools/injectable";
import ipcRendererInjectable from "./ipc-renderer/ipc-renderer.injectable";
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({
id: "get-value-from-registered-channel",
instantiate: (di) =>
getValueFromRegisteredChannel({ ipcRenderer: di.inject(ipcRendererInjectable) }),
instantiate: (di) => getValueFromRegisteredChannel({
ipcRenderer: di.inject(ipcRendererInjectable),
}),
});
export default getValueFromRegisteredChannelInjectable;

View File

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

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 { Router } from "react-router";
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 addSyncEntriesInjectable from "./initializers/add-sync-entries.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 type { DiRender } 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 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";
@ -98,8 +96,6 @@ describe("<Catalog />", () => {
await di.runSetups();
mockFs();
UserStore.createInstance();
ThemeStore.createInstance();
CatalogEntityDetailRegistry.createInstance();
render = renderFor(di);
@ -125,8 +121,6 @@ describe("<Catalog />", () => {
});
afterEach(() => {
UserStore.resetInstance();
ThemeStore.resetInstance();
CatalogEntityDetailRegistry.resetInstance();
jest.clearAllMocks();
jest.restoreAllMocks();

View File

@ -11,15 +11,20 @@ import { computed, makeObservable } from "mobx";
import { Icon } from "../icon";
import { SubHeader } from "../layout/sub-header";
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 type { ItemObject } from "../../../common/item.store";
import { Spinner } from "../spinner";
import { ThemeStore } from "../../theme.store";
import type { ThemeStore } from "../../themes/store";
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 { 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 {
className?: string;
@ -39,16 +44,23 @@ enum sortBy {
age = "age",
}
interface Dependencies {
themeStore: ThemeStore;
nodeStore: NodeStore;
eventStore: EventStore;
apiManager: ApiManager;
}
@observer
export class ClusterIssues extends React.Component<ClusterIssuesProps> {
constructor(props: ClusterIssuesProps) {
class NonInjectedClusterIssues extends React.Component<ClusterIssuesProps & Dependencies> {
constructor(props: ClusterIssuesProps & Dependencies) {
super(props);
makeObservable(this);
}
@computed get warnings(): Warning[] {
return [
...nodeStore.items.flatMap(node => (
...this.props.nodeStore.items.flatMap(node => (
node.getWarningConditions()
.map(({ message }) => ({
selfLink: node.selfLink,
@ -60,14 +72,14 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
ageMs: -node.getCreationTimestamp(),
}))
)),
...eventStore.getWarnings().map(warning => ({
...this.props.eventStore.getWarnings().map(warning => ({
getId: () => warning.involvedObject.uid,
getName: () => warning.involvedObject.name,
renderAge: () => <KubeObjectAge key="age" object={warning} />,
ageMs: -warning.getCreationTimestamp(),
message: warning.message,
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() {
const { warnings } = this;
if (!eventStore.isLoaded) {
if (!this.props.eventStore.isLoaded) {
return (
<Spinner center/>
);
@ -148,7 +160,7 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }}
sortSyncWithUrl={false}
getTableRow={this.getTableRow}
className={cssNames("box grow", ThemeStore.getInstance().activeTheme.type)}
className={cssNames("box grow", this.props.themeStore.activeTheme.type)}
>
<TableHead nowrap>
<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 { ClusterNoMetrics } from "./cluster-no-metrics";
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 { withInjectables } from "@ogre-tools/injectable-react";
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
import nodeStoreInjectable from "../+nodes/store.injectable";
import themeStoreInjectable from "../../themes/store.injectable";
function createLabels(rawLabelData: [string, number | undefined][]): string[] {
return rawLabelData.map(([key, value]) => `${key}: ${value?.toFixed(2) || "N/A"}`);
@ -29,11 +30,13 @@ function createLabels(rawLabelData: [string, number | undefined][]): string[] {
interface Dependencies {
clusterOverviewStore: ClusterOverviewStore;
nodeStore: NodeStore;
themeStore: ThemeStore;
}
const NonInjectedClusterPieCharts = observer(({
clusterOverviewStore,
nodeStore,
themeStore,
}: Dependencies) => {
const renderLimitWarning = () => {
return (
@ -51,7 +54,7 @@ const NonInjectedClusterPieCharts = observer(({
const { podUsage, podAllocatableCapacity, podCapacity } = data;
const cpuLimitsOverload = cpuLimits > cpuAllocatableCapacity;
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;
const cpuData: PieChartData = {
@ -258,5 +261,6 @@ export const ClusterPieCharts = withInjectables<Dependencies>(NonInjectedCluster
getProps: (di) => ({
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
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 { Button } from "../../button";
import { Notifications } from "../../notifications";
import { ThemeStore } from "../../../theme.store";
import type { ThemeStore } from "../../../themes/store";
import { apiManager } from "../../../../common/k8s-api/api-manager";
import { SubTitle } from "../../layout/sub-title";
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 type { KubeJsonApiData } from "../../../../common/k8s-api/kube-json-api";
import { entries } from "../../../../common/utils/objects";
import themeStoreInjectable from "../../../themes/store.injectable";
export interface ReleaseDetailsProps {
hideDetails(): void;
@ -50,6 +51,7 @@ interface Dependencies {
updateRelease: (name: string, namespace: string, payload: HelmReleaseUpdatePayload) => Promise<HelmReleaseUpdateDetails>;
createUpgradeChartTab: (release: HelmRelease) => void;
userSuppliedValuesAreShown: { toggle: () => void; value: boolean };
themeStore: ThemeStore;
}
@observer
@ -252,12 +254,12 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
}
render() {
const { hideDetails } = this.props;
const { hideDetails, themeStore } = this.props;
const release = this.props.release.get();
return (
<Drawer
className={cssNames("ReleaseDetails", ThemeStore.getInstance().activeTheme.type)}
className={cssNames("ReleaseDetails", themeStore.activeTheme.type)}
usePortal={true}
open={Boolean(release)}
title={release ? `Release: ${release.getName()}` : ""}
@ -285,5 +287,6 @@ export const ReleaseDetails = withInjectables<Dependencies, ReleaseDetailsProps>
userSuppliedValuesAreShown: di.inject(userSuppliedValuesAreShownInjectable),
updateRelease: di.inject(updateReleaseInjectable),
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 { mapValues } from "lodash";
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) ?? {};
if (!metrics || !object || !tab) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const id = object.getId();
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
const { chartCapacityColor } = themeStore.activeTheme.colors;
const {
memoryUsage,
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 { SubTitle } from "../layout/sub-title";
import { Select } from "../select";
import type { ThemeStore } from "../../theme.store";
import type { ThemeStore } from "../../themes/store";
import type { UserStore } from "../../../common/user-store";
import { Input } from "../input";
import { Switch } from "../switch";
@ -22,8 +22,8 @@ import { withInjectables } from "@ogre-tools/injectable-react";
import appPreferencesInjectable from "./app-preferences/app-preferences.injectable";
import { Preferences } from "./preferences";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import themeStoreInjectable from "../../theme-store.injectable";
import { defaultTheme } from "../../../common/vars";
import themeStoreInjectable from "../../themes/store.injectable";
import { defaultThemeId } from "../../../common/vars";
interface Dependencies {
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>;
@ -57,8 +57,6 @@ const extensionInstallRegistryOptions = [
const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, userStore, themeStore }) => {
const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
const extensionSettings = appPreferenceItems.get()
.filter((preference) => preference.showInPreferencesTab === "application");
const themeOptions = [
{
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,
})),
];
const extensionSettings = appPreferenceItems.get()
.filter((preference) => preference.showInPreferencesTab === "application");
return (
<Preferences data-testid="application-preferences-page">
@ -79,7 +79,7 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
<Select
options={themeOptions}
value={userStore.colorTheme}
onChange={value => userStore.colorTheme = value?.value ?? defaultTheme}
onChange={value => userStore.colorTheme = value?.value ?? defaultThemeId}
themeName="lens"
/>
</section>

View File

@ -10,11 +10,11 @@ import { SubTitle } from "../layout/sub-title";
import { Input, InputValidators } from "../input";
import { Switch } from "../switch";
import { Select } from "../select";
import type { ThemeStore } from "../../theme.store";
import type { ThemeStore } from "../../themes/store";
import { Preferences } from "./preferences";
import { withInjectables } from "@ogre-tools/injectable-react";
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";
interface Dependencies {

View File

@ -10,16 +10,26 @@ import { BarChart, memoryOptions } from "../chart";
import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-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) ?? {};
if (!metrics || !object || !tab) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const id = object.getId();
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
const { chartCapacityColor } = themeStore.activeTheme.colors;
const { diskUsage, diskCapacity } = metrics;
const usage = normalizeMetrics(diskUsage).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 { PieChart } from "../chart";
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 {
status: Partial<Record<string, number>>;
}
interface Dependencies {
themeStore: ThemeStore;
}
@observer
export class OverviewWorkloadStatus extends React.Component<OverviewWorkloadStatusProps> {
class NonInjectedOverviewWorkloadStatus extends React.Component<OverviewWorkloadStatusProps & Dependencies> {
private elem: HTMLElement | null = null;
renderChart() {
@ -37,7 +43,7 @@ export class OverviewWorkloadStatus extends React.Component<OverviewWorkloadStat
if (statuses.length === 0) {
chartData.datasets.push({
data: [1],
backgroundColor: [ThemeStore.getInstance().activeTheme.colors.pieChartDefaultColor],
backgroundColor: [this.props.themeStore.activeTheme.colors.pieChartDefaultColor],
label: "Empty",
});
} 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 { NoMetrics } from "../resource-metrics/no-metrics";
import { ResourceMetricsContext } from "../resource-metrics";
import { ThemeStore } from "../../theme.store";
import type { ThemeStore } from "../../themes/store";
import { mapValues } from "lodash";
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) ?? {};
if (!metrics || !object || !tab) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
const { chartCapacityColor } = themeStore.activeTheme.colors;
const {
cpuUsage,
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 { bytesToUnits, cssNames, isObject } from "../../utils";
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 assert from "assert";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
export interface BarChartProps extends ChartProps {
name?: string;
@ -24,7 +26,12 @@ export interface BarChartProps extends ChartProps {
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,
data,
className,
@ -32,8 +39,8 @@ export const BarChart = observer(({
plugins,
options: customOptions,
...settings
}: BarChartProps) => {
const { textColorPrimary, borderFaintColor, chartStripesColor } = ThemeStore.getInstance().activeTheme.colors;
}: Dependencies & BarChartProps) => {
const { textColorPrimary, borderFaintColor, chartStripesColor } = themeStore.activeTheme.colors;
const { datasets: rawDatasets = [], ...rest } = data;
const datasets = rawDatasets
.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"] => (
({ datasetIndex, index }, { datasets = [] }) => {
if (typeof datasetIndex !== "number" || typeof index !== "number") {

View File

@ -11,7 +11,9 @@ import ChartJS from "chart.js";
import type { ChartProps } from "./chart";
import { Chart } from "./chart";
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 {
}
@ -41,9 +43,19 @@ function getCutout(length: number | undefined): number {
}
}
export const PieChart = observer((props: PieChartProps) => {
const { data, className, options, ...chartProps } = props;
const { contentColor } = ThemeStore.getInstance().activeTheme.colors;
interface Dependencies {
themeStore: ThemeStore;
}
const NonInjectedPieChart = observer(({
themeStore,
data,
className,
options,
showChart,
...chartProps
}: Dependencies & PieChartProps) => {
const { contentColor } = themeStore.activeTheme.colors;
const opts: ChartOptions = {
maintainAspectRatio: false,
tooltips: {
@ -98,12 +110,20 @@ export const PieChart = observer((props: PieChartProps) => {
<Chart
className={cssNames("PieChart flex column align-center", className)}
data={data}
options={props.showChart ? {} : opts}
options={showChart ? {} : opts}
showChart={showChart}
{...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 }) {
return position;
};

View File

@ -11,8 +11,6 @@ import { DockTabs } from "../dock-tabs";
import type { DockStore, DockTab } from "../dock/store";
import { TabKind } from "../dock/store";
import { noop } from "../../../utils";
import { ThemeStore } from "../../../theme.store";
import { UserStore } from "../../../../common/user-store";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import dockStoreInjectable from "../dock/store.injectable";
import type { DiRender } from "../../test-utils/renderFor";
@ -92,16 +90,11 @@ describe("<DockTabs />", () => {
dockStore = di.inject(dockStoreInjectable);
UserStore.createInstance();
ThemeStore.createInstance();
await dockStore.whenReady;
dockStore.tabs = initialTabs;
});
afterEach(() => {
ThemeStore.resetInstance();
UserStore.resetInstance();
// 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");
});

View File

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

View File

@ -10,6 +10,7 @@ import type { TerminalApi } from "../../../api/terminal-api";
import terminalSpawningPoolInjectable from "./terminal-spawning-pool.injectable";
import terminalConfigInjectable from "../../../../common/user-store/terminal-config.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;
@ -20,6 +21,7 @@ const createTerminalInjectable = getInjectable({
spawningPool: di.inject(terminalSpawningPoolInjectable),
terminalConfig: di.inject(terminalConfigInjectable),
terminalCopyOnSelect: di.inject(terminalCopyOnSelectInjectable),
themeStore: di.inject(themeStoreInjectable),
};
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 { TerminalApi } 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 { isMac } from "../../../../common/vars";
import { once } from "lodash";
@ -24,6 +24,7 @@ export interface TerminalDependencies {
readonly spawningPool: HTMLElement;
readonly terminalConfig: IComputedValue<TerminalConfig>;
readonly terminalCopyOnSelect: IComputedValue<boolean>;
readonly themeStore: ThemeStore;
}
export interface TerminalArguments {
@ -94,7 +95,7 @@ export class Terminal {
window.addEventListener("resize", this.onResize);
this.disposer.push(
reaction(() => ThemeStore.getInstance().xtermColors, colors => {
reaction(() => this.dependencies.themeStore.xtermColors, colors => {
this.xterm?.setOption("theme", colors);
}, {
fireImmediately: true,

View File

@ -10,12 +10,13 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { cssNames } from "../../../utils";
import type { Terminal } from "./terminal";
import type { TerminalStore } from "./store";
import { ThemeStore } from "../../../theme.store";
import type { ThemeStore } from "../../../themes/store";
import type { DockTab, DockStore } from "../dock/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "../dock/store.injectable";
import terminalStoreInjectable from "./store.injectable";
import assert from "assert";
import themeStoreInjectable from "../../../themes/store.injectable";
export interface TerminalWindowProps {
tab: DockTab;
@ -24,6 +25,7 @@ export interface TerminalWindowProps {
interface Dependencies {
dockStore: DockStore;
terminalStore: TerminalStore;
themeStore: ThemeStore;
}
@observer
@ -66,7 +68,7 @@ class NonInjectedTerminalWindow extends React.Component<TerminalWindowProps & De
render() {
return (
<div
className={cssNames("TerminalWindow", ThemeStore.getInstance().activeTheme.type)}
className={cssNames("TerminalWindow", this.props.themeStore.activeTheme.type)}
ref={elem => this.elem = elem}
/>
);
@ -75,9 +77,10 @@ class NonInjectedTerminalWindow extends React.Component<TerminalWindowProps & De
export const TerminalWindow = withInjectables<Dependencies, TerminalWindowProps>(NonInjectedTerminalWindow, {
getProps: (di, props) => ({
...props,
dockStore: di.inject(dockStoreInjectable),
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 { renderFor } from "../../test-utils/renderFor";
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
import { ThemeStore } from "../../../theme.store";
import { ConfirmDialog } from "../../confirm-dialog";
import { UserStore } from "../../../../common/user-store";
import mockFs from "mock-fs";
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";
@ -54,15 +52,10 @@ describe("<HotbarRemoveCommand />", () => {
di.permitSideEffects(appVersionInjectable);
render = renderFor(di);
UserStore.createInstance();
ThemeStore.createInstance();
});
afterEach(() => {
mockFs.restore();
ThemeStore.resetInstance();
UserStore.resetInstance();
});
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 { Filter } 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 { MenuItem } from "../menu";
import { Checkbox } from "../checkbox";
import { UserStore } from "../../../common/user-store";
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[];
tableId?: string;
className: IClassName;
getItems: () => I[];
store: ItemListStore<I, PreLoadStores>;
getItems: () => Item[];
store: ItemListStore<Item, PreLoadStores>;
getIsReady: () => boolean; // show loading indicator while not ready
isSelectable?: boolean; // show checkbox in rows for selecting items
isConfigurable?: boolean;
copyClassNameFromHeadCells?: boolean;
sortingCallbacks?: TableSortCallbacks<I>;
tableProps?: Partial<TableProps<I>>; // low-level table configuration
sortingCallbacks?: TableSortCallbacks<Item>;
tableProps?: Partial<TableProps<Item>>; // low-level table configuration
renderTableHeader?: (TableCellProps | undefined | null)[];
renderTableContents: (item: I) => (ReactNode | TableCellProps)[];
renderItemMenu?: (item: I, store: ItemListStore<I, PreLoadStores>) => ReactNode;
customizeTableRowProps?: (item: I) => Partial<TableRowProps<I>>;
renderTableContents: (item: Item) => (ReactNode | TableCellProps)[];
renderItemMenu?: (item: Item, store: ItemListStore<Item, PreLoadStores>) => ReactNode;
customizeTableRowProps?: (item: Item) => Partial<TableRowProps<Item>>;
addRemoveButtons?: Partial<AddRemoveButtonsProps>;
virtual?: boolean;
// item details view
hasDetailsView?: boolean;
detailsItem?: I;
onDetails?: (item: I) => void;
detailsItem?: Item;
onDetails?: (item: Item) => void;
// other
customizeRemoveDialog?: (selectedItems: I[]) => Partial<ConfirmDialogParams>;
customizeRemoveDialog?: (selectedItems: Item[]) => Partial<ConfirmDialogParams>;
/**
* Message to display when a store failed to load
@ -64,9 +66,13 @@ export interface ItemListLayoutContentProps<I extends ItemObject, PreLoadStores
failedToLoadMessage?: React.ReactNode;
}
interface Dependencies {
themeStore: ThemeStore;
}
@observer
export class ItemListLayoutContent<I extends ItemObject, PreLoadStores extends boolean> extends React.Component<ItemListLayoutContentProps<I, PreLoadStores>> {
constructor(props: ItemListLayoutContentProps<I, PreLoadStores>) {
class NonInjectedItemListLayoutContent<Item extends ItemObject, PreLoadStores extends boolean> extends React.Component<ItemListLayoutContentProps<Item, PreLoadStores> & Dependencies> {
constructor(props: ItemListLayoutContentProps<Item, PreLoadStores> & Dependencies) {
super(props);
makeObservable(this);
autoBind(this);
@ -76,11 +82,11 @@ export class ItemListLayoutContent<I extends ItemObject, PreLoadStores extends b
return this.props.store.failedLoading;
}
renderRow(item: I) {
renderRow(item: Item) {
return this.getTableRow(item);
}
getTableRow(item: I) {
getTableRow(item: Item) {
const {
isSelectable, renderTableHeader, renderTableContents, renderItemMenu,
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 visibleMaxNamesCount = 5;
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() {
const {
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks,
detailsItem, className, tableProps = {}, tableId, getItems,
detailsItem, className, tableProps = {}, tableId, getItems, themeStore,
} = this.props;
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 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 { autoBind, cssNames, disposer } from "../../utils";
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";
export type MonacoEditorId = string;
@ -35,22 +37,21 @@ export interface MonacoEditorProps {
onModelChange?(model: editor.ITextModel, prev?: editor.ITextModel): void;
}
export const defaultEditorProps: Partial<MonacoEditorProps> = {
language: "yaml",
get theme(): MonacoTheme {
// theme for monaco-editor defined in `src/renderer/themes/lens-*.json`
return ThemeStore.getInstance().activeTheme.monacoTheme;
},
};
interface Dependencies {
themeStore: ThemeStore;
}
export function createMonacoUri(id: MonacoEditorId): Uri {
return Uri.file(`/monaco-editor/${id}`);
}
const monacoViewStates = new WeakMap<Uri, editor.ICodeEditorViewState>();
@observer
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}`);
}
class NonInjectedMonacoEditor extends React.Component<MonacoEditorProps & Dependencies> {
static defaultProps = {
language: "yaml" as const,
};
private staticId = `editor-id#${Math.round(1e7 * Math.random())}`;
private dispose = disposer();
@ -60,7 +61,7 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
@observable readonly dimensions: { width?: number; height?: number } = {};
@observable unmounting = false;
constructor(props: MonacoEditorProps) {
constructor(props: MonacoEditorProps & Dependencies) {
super(props);
makeObservable(this);
autoBind(this);
@ -70,8 +71,12 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
return this.props.id ?? this.staticId;
}
@computed get theme() {
return this.props.theme ?? this.props.themeStore.activeTheme.monacoTheme;
}
@computed get model(): editor.ITextModel {
const uri = MonacoEditor.createUri(this.id);
const uri = createMonacoUri(this.id);
const model = editor.getModel(uri);
if (model) {
@ -141,12 +146,12 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
const viewState = this.editor?.saveViewState();
if (viewState) {
MonacoEditor.viewStates.set(model.uri, viewState);
monacoViewStates.set(model.uri, viewState);
}
}
protected restoreViewState(model: editor.ITextModel) {
const viewState = MonacoEditor.viewStates.get(model.uri);
const viewState = monacoViewStates.get(model.uri);
if (viewState) {
this.editor?.restoreViewState(viewState);
@ -177,7 +182,8 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
if (!this.containerElem || this.editor || this.unmounting) {
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, {
model: this.model,
@ -214,7 +220,7 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
this.dispose.push(
reaction(() => this.model, this.onModelChange),
reaction(() => this.props.theme, theme => {
reaction(() => this.theme, theme => {
if (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 type { SelectOption } from "./select";
import { Select } from "./select";
import { UserStore } from "../../../common/user-store";
import { ThemeStore } from "../../theme.store";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { DiContainer } from "@ogre-tools/injectable";
import type { DiRender } from "../test-utils/renderFor";
@ -42,15 +40,9 @@ describe("<Select />", () => {
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
UserStore.createInstance();
ThemeStore.createInstance();
});
afterEach(() => {
ThemeStore.resetInstance();
UserStore.resetInstance();
mockFs.restore();
});

View File

@ -12,9 +12,11 @@ import { action, computed, makeObservable } from "mobx";
import { observer } from "mobx-react";
import ReactSelect, { components, createFilter } 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 type { SetRequired } from "type-fest";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
const { Menu } = components;
@ -60,26 +62,30 @@ const defaultFilter = createFilter({
},
});
interface Dependencies {
themeStore: ThemeStore;
}
@observer
export class Select<
class NonInjectedSelect<
Value,
Option extends SelectOption<Value>,
IsMulti extends boolean = false,
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 = {
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);
makeObservable(this);
autoBind(this);
}
@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}`;
}
@ -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.
*/
import type { Theme } from "../theme.store";
import type { Theme } from "./store";
const lensDarkTheme: Theme = {
"name": "Dark",

View File

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