Cascade different refactorings to eliminate global shared state from app paths

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne SavolainenandMikko Aspiala committed 2022-01-05 13:26:49 +02:00
1 parent 908a40975e
commit 9d33fff906
336 files changed
+9105 -3307

No files matched your search

@@ -20,16 +20,20 @@
*/
import React from "react";
import { fireEvent, render } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import fse from "fs-extra";
import { DockTabs } from "../dock-tabs";
import { dockStore, DockTab, TabKind } from "../dock.store";
import { DockStore, DockTab, TabKind } from "../dock-store/dock.store";
import { noop } from "../../../utils";
import { ThemeStore } from "../../../theme.store";
import { TerminalStore } from "../terminal.store";
import { UserStore } from "../../../../common/user-store";
import { AppPaths } from "../../../../common/app-paths";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import directoryForUserDataInjectable
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
jest.mock("electron", () => ({
app: {
@@ -46,7 +50,6 @@ jest.mock("electron", () => ({
handle: jest.fn(),
},
}));
AppPaths.init();
const initialTabs: DockTab[] = [
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false },
@@ -56,7 +59,7 @@ const initialTabs: DockTab[] = [
{ id: "logs", kind: TabKind.POD_LOGS, title: "Logs", pinned: false },
];
const getComponent = () => (
const getComponent = (dockStore: DockStore) => (
<DockTabs
tabs={dockStore.tabs}
selectedTab={dockStore.selectedTab}
@@ -65,40 +68,56 @@ const getComponent = () => (
/>
);
const renderTabs = () => render(getComponent());
const getTabKinds = () => dockStore.tabs.map(tab => tab.kind);
const getTabKinds = (dockStore: DockStore) => dockStore.tabs.map((tab) => tab.kind);
describe("<DockTabs />", () => {
let dockStore: DockStore;
let render: DiRender;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
render = renderFor(di);
di.override(
directoryForUserDataInjectable,
() => "some-test-suite-specific-directory-for-user-data",
);
await di.runSetups();
dockStore = di.inject(dockStoreInjectable);
UserStore.createInstance();
ThemeStore.createInstance();
TerminalStore.createInstance();
await dockStore.whenReady;
dockStore.tabs = initialTabs;
});
afterEach(() => {
ThemeStore.resetInstance();
TerminalStore.resetInstance();
UserStore.resetInstance();
fse.remove("tmp");
// 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");
});
it("renders w/o errors", () => {
const { container } = renderTabs();
const { container } = render(getComponent(dockStore));
expect(container).toBeInstanceOf(HTMLElement);
});
it("has 6 tabs (1 tab is initial terminal)", () => {
const { container } = renderTabs();
const { container } = render(getComponent(dockStore));
const tabs = container.querySelectorAll(".Tab");
expect(tabs.length).toBe(initialTabs.length);
});
it("opens a context menu", () => {
const { container, getByText } = renderTabs();
const { container, getByText } = render(getComponent(dockStore));
const tab = container.querySelector(".Tab");
fireEvent.contextMenu(tab);
@@ -106,17 +125,22 @@ describe("<DockTabs />", () => {
});
it("closes selected tab", () => {
const { container, getByText, rerender } = renderTabs();
const { container, getByText, rerender } = render(
getComponent(dockStore),
);
const tab = container.querySelector(".Tab");
fireEvent.contextMenu(tab);
fireEvent.click(getByText("Close"));
rerender(getComponent());
rerender(getComponent(dockStore));
const tabs = container.querySelectorAll(".Tab");
expect(tabs.length).toBe(initialTabs.length - 1);
expect(getTabKinds()).toEqual([
expect(getTabKinds(dockStore)).toEqual([
TabKind.CREATE_RESOURCE,
TabKind.EDIT_RESOURCE,
TabKind.INSTALL_CHART,
@@ -125,42 +149,42 @@ describe("<DockTabs />", () => {
});
it("closes other tabs", () => {
const { container, getByText, rerender } = renderTabs();
const { container, getByText, rerender } = render(getComponent(dockStore));
const tab = container.querySelectorAll(".Tab")[3];
fireEvent.contextMenu(tab);
fireEvent.click(getByText("Close other tabs"));
rerender(getComponent());
rerender(getComponent(dockStore));
const tabs = container.querySelectorAll(".Tab");
expect(tabs.length).toBe(1);
expect(getTabKinds()).toEqual([initialTabs[3].kind]);
expect(getTabKinds(dockStore)).toEqual([initialTabs[3].kind]);
});
it("closes all tabs", () => {
const { container, getByText, rerender } = renderTabs();
const { container, getByText, rerender } = render(getComponent(dockStore));
const tab = container.querySelector(".Tab");
fireEvent.contextMenu(tab);
const command = getByText("Close all tabs");
fireEvent.click(command);
rerender(getComponent());
rerender(getComponent(dockStore));
const tabs = container.querySelectorAll(".Tab");
expect(tabs.length).toBe(0);
});
it("closes tabs to the right", () => {
const { container, getByText, rerender } = renderTabs();
const { container, getByText, rerender } = render(getComponent(dockStore));
const tab = container.querySelectorAll(".Tab")[3]; // 4th of 5
fireEvent.contextMenu(tab);
fireEvent.click(getByText("Close tabs to the right"));
rerender(getComponent());
rerender(getComponent(dockStore));
expect(getTabKinds()).toEqual(
expect(getTabKinds(dockStore)).toEqual(
initialTabs.slice(0, 4).map(tab => tab.kind),
);
});
@@ -169,7 +193,7 @@ describe("<DockTabs />", () => {
dockStore.tabs = [{
id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false,
}];
const { container, getByText } = renderTabs();
const { container, getByText } = render(getComponent(dockStore));
const tab = container.querySelector(".Tab");
fireEvent.contextMenu(tab);
@@ -185,7 +209,7 @@ describe("<DockTabs />", () => {
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false },
{ id: "logs", kind: TabKind.POD_LOGS, title: "Pod Logs", pinned: false },
];
const { container, getByText } = renderTabs();
const { container, getByText } = render(getComponent(dockStore));
const tab = container.querySelectorAll(".Tab")[1];
fireEvent.contextMenu(tab);
@@ -21,17 +21,20 @@
import React from "react";
import "@testing-library/jest-dom/extend-expect";
import { render } from "@testing-library/react";
import selectEvent from "react-select-event";
import { Pod } from "../../../../common/k8s-api/endpoints";
import { LogResourceSelector } from "../log-resource-selector";
import type { LogTabData } from "../log-tab.store";
import type { LogTabData } from "../log-tab-store/log-tab.store";
import { dockerPod, deploymentPod1 } from "./pod.mock";
import { ThemeStore } from "../../../theme.store";
import { UserStore } from "../../../../common/user-store";
import mockFs from "mock-fs";
import { AppPaths } from "../../../../common/app-paths";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import directoryForUserDataInjectable
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
jest.mock("electron", () => ({
app: {
@@ -49,8 +52,6 @@ jest.mock("electron", () => ({
},
}));
AppPaths.init();
const getComponent = (tabData: LogTabData) => {
return (
<LogResourceSelector
@@ -84,10 +85,21 @@ const getFewPodsTabData = (): LogTabData => {
};
describe("<LogResourceSelector />", () => {
beforeEach(() => {
let render: DiRender;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
render = renderFor(di);
await di.runSetups();
mockFs({
"tmp": {},
});
UserStore.createInstance();
ThemeStore.createInstance();
});
@@ -23,49 +23,47 @@ import { podsStore } from "../../+workloads-pods/pods.store";
import { UserStore } from "../../../../common/user-store";
import { Pod } from "../../../../common/k8s-api/endpoints";
import { ThemeStore } from "../../../theme.store";
import { dockStore } from "../dock.store";
import { logTabStore } from "../log-tab.store";
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
import fse from "fs-extra";
import { mockWindow } from "../../../../../__mocks__/windowMock";
import { AppPaths } from "../../../../common/app-paths";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable";
import type { LogTabStore } from "../log-tab-store/log-tab.store";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import type { DockStore } from "../dock-store/dock.store";
import directoryForUserDataInjectable
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import mockFs from "mock-fs";
mockWindow();
jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
getName: () => "lens",
setName: jest.fn(),
setPath: jest.fn(),
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
},
ipcMain: {
on: jest.fn(),
handle: jest.fn(),
},
}));
AppPaths.init();
podsStore.items.push(new Pod(dockerPod));
podsStore.items.push(new Pod(deploymentPod1));
podsStore.items.push(new Pod(deploymentPod2));
describe("log tab store", () => {
beforeEach(() => {
let logTabStore: LogTabStore;
let dockStore: DockStore;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs();
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
await di.runSetups();
dockStore = di.inject(dockStoreInjectable);
logTabStore = di.inject(logTabStoreInjectable);
UserStore.createInstance();
ThemeStore.createInstance();
});
afterEach(() => {
logTabStore.reset();
dockStore.reset();
UserStore.resetInstance();
ThemeStore.resetInstance();
fse.remove("tmp");
mockFs.restore();
});
it("creates log tab without sibling pods", () => {
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { createInstallChartTab } from "./create-install-chart-tab";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import installChartStoreInjectable from "../install-chart-store/install-chart-store.injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
const createInstallChartTabInjectable = getInjectable({
instantiate: (di) => createInstallChartTab({
installChartStore: di.inject(installChartStoreInjectable),
createDockTab: di.inject(dockStoreInjectable).createTab,
}),
lifecycle: lifecycleEnum.singleton,
});
export default createInstallChartTabInjectable;
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api";
import {
DockTab,
DockTabCreate,
DockTabCreateSpecific,
TabKind,
} from "../dock-store/dock.store";
import type { InstallChartStore } from "../install-chart-store/install-chart.store";
interface Dependencies {
createDockTab: (rawTab: DockTabCreate, addNumber: boolean) => DockTab;
installChartStore: InstallChartStore;
}
export const createInstallChartTab =
({ createDockTab, installChartStore }: Dependencies) =>
(chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => {
const { name, repo, version } = chart;
const tab = createDockTab(
{
title: `Helm Install: ${repo}/${name}`,
...tabParams,
kind: TabKind.INSTALL_CHART,
},
false,
);
installChartStore.setData(tab.id, {
name,
repo,
version,
namespace: "default",
releaseName: "",
description: "",
});
return tab;
};
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import { CreateResourceStore } from "./create-resource.store";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const createResourceStoreInjectable = getInjectable({
instantiate: (di) => new CreateResourceStore({
dockStore: di.inject(dockStoreInjectable),
createStorage: di.inject(createStorageInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default createResourceStoreInjectable;
@@ -25,15 +25,21 @@ import os from "os";
import groupBy from "lodash/groupBy";
import filehound from "filehound";
import { watch } from "chokidar";
import { autoBind } from "../../utils";
import { DockTabStore } from "./dock-tab.store";
import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store";
import { autoBind, StorageHelper } from "../../../utils";
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
import type { DockStore } from "../dock-store/dock.store";
interface Dependencies {
dockStore: DockStore,
createStorage:<T> (storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
}
export class CreateResourceStore extends DockTabStore<string> {
constructor() {
super({
constructor(protected dependencies: Dependencies) {
super(dependencies, {
storageKey: "create_resource",
});
autoBind(this);
fs.ensureDirSync(this.userTemplatesFolder);
}
@@ -78,13 +84,3 @@ export class CreateResourceStore extends DockTabStore<string> {
});
}
}
export const createResourceStore = new CreateResourceStore();
export function createResourceTab(tabParams: DockTabCreateSpecific = {}) {
return dockStore.createTab({
title: "Create resource",
...tabParams,
kind: TabKind.CREATE_RESOURCE,
});
}
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { createResourceTab } from "./create-resource-tab";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
const createResourceTabInjectable = getInjectable({
instantiate: (di) => createResourceTab({
dockStore: di.inject(dockStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default createResourceTabInjectable;
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store";
interface Dependencies {
dockStore: DockStore
}
export const createResourceTab =
({ dockStore }: Dependencies) =>
(tabParams: DockTabCreateSpecific = {}) =>
dockStore.createTab({
title: "Create resource",
...tabParams,
kind: TabKind.CREATE_RESOURCE,
});
@@ -28,8 +28,8 @@ import { GroupSelectOption, Select, SelectOption } from "../select";
import yaml from "js-yaml";
import { makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
import { createResourceStore } from "./create-resource.store";
import type { DockTab } from "./dock.store";
import type { CreateResourceStore } from "./create-resource-store/create-resource.store";
import type { DockTab } from "./dock-store/dock.store";
import { EditorPanel } from "./editor-panel";
import { InfoPanel } from "./info-panel";
import * as resourceApplierApi from "../../../common/k8s-api/endpoints/resource-applier.api";
@@ -40,25 +40,32 @@ import { getDetailsUrl } from "../kube-detail-params";
import { apiManager } from "../../../common/k8s-api/api-manager";
import { prevDefault } from "../../utils";
import { navigate } from "../../navigation";
import { withInjectables } from "@ogre-tools/injectable-react";
import createResourceStoreInjectable
from "./create-resource-store/create-resource-store.injectable";
interface Props {
tab: DockTab;
}
interface Dependencies {
createResourceStore: CreateResourceStore
}
@observer
export class CreateResource extends React.Component<Props> {
class NonInjectedCreateResource extends React.Component<Props & Dependencies> {
@observable currentTemplates: Map<string, SelectOption> = new Map();
@observable error = "";
@observable templates: GroupSelectOption<SelectOption>[] = [];
constructor(props: Props) {
constructor(props: Props & Dependencies) {
super(props);
makeObservable(this);
}
componentDidMount() {
createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v));
createResourceStore.watchUserTemplates(() => createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v)));
this.props.createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v));
this.props.createResourceStore.watchUserTemplates(() => this.props.createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v)));
}
updateGroupSelectOptions(templates: Record<string, string[]>) {
@@ -77,7 +84,7 @@ export class CreateResource extends React.Component<Props> {
}
get data() {
return createResourceStore.getData(this.tabId);
return this.props.createResourceStore.getData(this.tabId);
}
get currentTemplate() {
@@ -86,7 +93,7 @@ export class CreateResource extends React.Component<Props> {
onChange = (value: string) => {
this.error = ""; // reset first, validation goes later
createResourceStore.setData(this.tabId, value);
this.props.createResourceStore.setData(this.tabId, value);
};
onError = (error: Error | string) => {
@@ -96,7 +103,7 @@ export class CreateResource extends React.Component<Props> {
onSelectTemplate = (item: SelectOption) => {
this.currentTemplates.set(this.tabId, item);
fs.readFile(item.value, "utf8").then(v => {
createResourceStore.setData(this.tabId, v);
this.props.createResourceStore.setData(this.tabId, v);
});
};
@@ -180,3 +187,14 @@ export class CreateResource extends React.Component<Props> {
);
}
}
export const CreateResource = withInjectables<Dependencies, Props>(
NonInjectedCreateResource,
{
getProps: (di, props) => ({
createResourceStore: di.inject(createResourceStoreInjectable),
...props,
}),
},
);
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { createTerminalTab } from "./create-terminal-tab";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
const createTerminalTabInjectable = getInjectable({
instantiate: (di) => createTerminalTab({
dockStore: di.inject(dockStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default createTerminalTabInjectable;
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import {
DockStore,
DockTabCreateSpecific,
TabKind,
} from "../dock-store/dock.store";
interface Dependencies {
dockStore: DockStore;
}
export const createTerminalTab =
({ dockStore }: Dependencies) =>
(tabParams: DockTabCreateSpecific = {}) =>
dockStore.createTab({
title: `Terminal`,
...tabParams,
kind: TabKind.TERMINAL,
});
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { createUpgradeChartTab } from "./create-upgrade-chart-tab";
import upgradeChartStoreInjectable from "../upgrade-chart-store/upgrade-chart-store.injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
const createUpgradeChartTabInjectable = getInjectable({
instantiate: (di) => createUpgradeChartTab({
upgradeChartStore: di.inject(upgradeChartStoreInjectable),
dockStore: di.inject(dockStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default createUpgradeChartTabInjectable;
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store";
import type { UpgradeChartStore } from "../upgrade-chart-store/upgrade-chart.store";
interface Dependencies {
upgradeChartStore: UpgradeChartStore;
dockStore: DockStore
}
export const createUpgradeChartTab =
({ upgradeChartStore, dockStore }: Dependencies) =>
(release: HelmRelease, tabParams: DockTabCreateSpecific = {}) => {
let tab = upgradeChartStore.getTabByRelease(release.getName());
if (tab) {
dockStore.open();
dockStore.selectTab(tab.id);
}
if (!tab) {
tab = dockStore.createTab(
{
title: `Helm Upgrade: ${release.getName()}`,
...tabParams,
kind: TabKind.UPGRADE_CHART,
},
false,
);
upgradeChartStore.setData(tab.id, {
releaseName: release.getName(),
releaseNamespace: release.getNs(),
});
}
return tab;
};
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { DockStorageState, DockStore, TabKind } from "./dock.store";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const dockStoreInjectable = getInjectable({
instantiate: (di) => {
const createStorage = di.inject(createStorageInjectable);
const storage = createStorage<DockStorageState>("dock", {
height: 300,
tabs: [
{
id: "terminal",
kind: TabKind.TERMINAL,
title: "Terminal",
pinned: false,
},
],
});
return new DockStore({
storage,
});
},
lifecycle: lifecycleEnum.singleton,
});
export default dockStoreInjectable;
@@ -21,7 +21,7 @@
import * as uuid from "uuid";
import { action, comparer, computed, makeObservable, observable, reaction, runInAction } from "mobx";
import { autoBind, createStorage } from "../../utils";
import { autoBind, StorageHelper } from "../../../utils";
import throttle from "lodash/throttle";
export type TabId = string;
@@ -113,8 +113,12 @@ export interface DockTabCloseEvent {
tabId: TabId; // closed tab id
}
interface Dependencies {
storage: StorageHelper<DockStorageState>
}
export class DockStore implements DockStorageState {
constructor() {
constructor(private dependencies: Dependencies) {
makeObservable(this);
autoBind(this);
this.init();
@@ -123,45 +127,38 @@ export class DockStore implements DockStorageState {
readonly minHeight = 100;
@observable fullSize = false;
private storage = createStorage<DockStorageState>("dock", {
height: 300,
tabs: [
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false },
],
});
get whenReady() {
return this.storage.whenReady;
return this.dependencies.storage.whenReady;
}
get isOpen(): boolean {
return this.storage.get().isOpen;
return this.dependencies.storage.get().isOpen;
}
set isOpen(isOpen: boolean) {
this.storage.merge({ isOpen });
this.dependencies.storage.merge({ isOpen });
}
get height(): number {
return this.storage.get().height;
return this.dependencies.storage.get().height;
}
set height(height: number) {
this.storage.merge({
this.dependencies.storage.merge({
height: Math.max(this.minHeight, Math.min(height || this.minHeight, this.maxHeight)),
});
}
get tabs(): DockTab[] {
return this.storage.get().tabs;
return this.dependencies.storage.get().tabs;
}
set tabs(tabs: DockTab[]) {
this.storage.merge({ tabs });
this.dependencies.storage.merge({ tabs });
}
get selectedTabId(): TabId | undefined {
return this.storage.get().selectedTabId
return this.dependencies.storage.get().selectedTabId
|| (
this.tabs.length > 0
? this.tabs[0]?.id
@@ -172,7 +169,7 @@ export class DockStore implements DockStorageState {
set selectedTabId(tabId: TabId) {
if (tabId && !this.getTabById(tabId)) return; // skip invalid ids
this.storage.merge({ selectedTabId: tabId });
this.dependencies.storage.merge({ selectedTabId: tabId });
}
@computed get selectedTab() {
@@ -206,7 +203,7 @@ export class DockStore implements DockStorageState {
}
onTabClose(callback: (evt: DockTabCloseEvent) => void, opts: { fireImmediately?: boolean } = {}) {
return reaction(() => dockStore.tabs.map(tab => tab.id), (tabs: TabId[], prevTabs?: TabId[]) => {
return reaction(() => this.tabs.map(tab => tab.id), (tabs: TabId[], prevTabs?: TabId[]) => {
if (!Array.isArray(prevTabs)) {
return; // tabs not yet modified
}
@@ -292,7 +289,7 @@ export class DockStore implements DockStorageState {
}
@action
createTab(rawTabDesc: DockTabCreate, addNumber = true): DockTab {
createTab = (rawTabDesc: DockTabCreate, addNumber = true): DockTab => {
const {
id = uuid.v4(),
kind,
@@ -322,7 +319,7 @@ export class DockStore implements DockStorageState {
this.open();
return tab;
}
};
@action
closeTab(tabId: TabId) {
@@ -381,8 +378,6 @@ export class DockStore implements DockStorageState {
@action
reset() {
this.storage?.reset();
this.dependencies.storage?.reset();
}
}
export const dockStore = new DockStore();
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { DockTabStore, DockTabStoreOptions } from "./dock-tab.store";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const createDockTabStoreInjectable = getInjectable({
instantiate: (di) => {
const dependencies = {
dockStore: di.inject(dockStoreInjectable),
createStorage: di.inject(createStorageInjectable),
};
return <T>(options: DockTabStoreOptions = {}) => new DockTabStore<T>(dependencies, options);
},
lifecycle: lifecycleEnum.singleton,
});
export default createDockTabStoreInjectable;
@@ -20,8 +20,8 @@
*/
import { autorun, observable, reaction } from "mobx";
import { autoBind, createStorage, StorageHelper, toJS } from "../../utils";
import { dockStore, TabId } from "./dock.store";
import { autoBind, StorageHelper, toJS } from "../../../utils";
import type { DockStore, TabId } from "../dock-store/dock.store";
export interface DockTabStoreOptions {
autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events, default: true
@@ -30,11 +30,16 @@ export interface DockTabStoreOptions {
export type DockTabStorageState<T> = Record<TabId, T>;
interface Dependencies {
dockStore: DockStore
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
}
export class DockTabStore<T> {
protected storage?: StorageHelper<DockTabStorageState<T>>;
protected data = observable.map<TabId, T>();
constructor(protected options: DockTabStoreOptions = {}) {
constructor(protected dependencies: Dependencies, protected options: DockTabStoreOptions) {
autoBind(this);
this.options = {
@@ -52,7 +57,7 @@ export class DockTabStore<T> {
// auto-save to local-storage
if (storageKey) {
this.storage = createStorage(storageKey, {});
this.storage = this.dependencies.createStorage<T>(storageKey, {});
this.storage.whenReady.then(() => {
this.data.replace(this.storage.get());
reaction(() => this.toJSON(), data => this.storage.set(data));
@@ -61,7 +66,7 @@ export class DockTabStore<T> {
// clear data for closed tabs
autorun(() => {
const currentTabs = dockStore.tabs.map(tab => tab.id);
const currentTabs = this.dependencies.dockStore.tabs.map(tab => tab.id);
Array.from(this.data.keys()).forEach(tabId => {
if (!currentTabs.includes(tabId)) {
+23 -6
View File
@@ -24,22 +24,28 @@ import "./dock-tab.scss";
import React from "react";
import { observer } from "mobx-react";
import { boundMethod, cssNames, prevDefault, isMiddleClick } from "../../utils";
import { dockStore, DockTab as DockTabModel } from "./dock.store";
import type { DockStore, DockTab as DockTabModel } from "./dock-store/dock.store";
import { Tab, TabProps } from "../tabs";
import { Icon } from "../icon";
import { Menu, MenuItem } from "../menu";
import { observable, makeObservable } from "mobx";
import { isMac } from "../../../common/vars";
import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock-store/dock-store.injectable";
export interface DockTabProps extends TabProps<DockTabModel> {
moreActions?: React.ReactNode;
}
interface Dependencies {
dockStore: DockStore
}
@observer
export class DockTab extends React.Component<DockTabProps> {
class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
@observable menuVisible = false;
constructor(props: DockTabProps) {
constructor(props: DockTabProps & Dependencies) {
super(props);
makeObservable(this);
}
@@ -50,11 +56,11 @@ export class DockTab extends React.Component<DockTabProps> {
@boundMethod
close() {
dockStore.closeTab(this.tabId);
this.props.dockStore.closeTab(this.tabId);
}
renderMenu() {
const { closeTab, closeAllTabs, closeOtherTabs, closeTabsToTheRight, tabs, getTabIndex } = dockStore;
const { closeTab, closeAllTabs, closeOtherTabs, closeTabsToTheRight, tabs, getTabIndex } = this.props.dockStore;
const closeAllDisabled = tabs.length === 1;
const closeOtherDisabled = tabs.length === 1;
const closeRightDisabled = getTabIndex(this.tabId) === tabs.length - 1;
@@ -86,7 +92,7 @@ export class DockTab extends React.Component<DockTabProps> {
}
render() {
const { className, moreActions, ...tabProps } = this.props;
const { className, moreActions, dockStore, ...tabProps } = this.props;
const { title, pinned } = tabProps.value;
const label = (
<div className="flex gaps align-center" onAuxClick={isMiddleClick(prevDefault(this.close))}>
@@ -116,3 +122,14 @@ export class DockTab extends React.Component<DockTabProps> {
);
}
}
export const DockTab = withInjectables<Dependencies, DockTabProps>(
NonInjectedDockTab,
{
getProps: (di, props) => ({
dockStore: di.inject(dockStoreInjectable),
...props,
}),
},
);
+2 -2
View File
@@ -24,8 +24,8 @@ import React, { Fragment } from "react";
import { Icon } from "../icon";
import { Tabs } from "../tabs/tabs";
import { DockTab } from "./dock-tab";
import type { DockTab as DockTabModel } from "./dock.store";
import { TabKind } from "./dock.store";
import type { DockTab as DockTabModel } from "./dock-store/dock.store";
import { TabKind } from "./dock-store/dock.store";
import { TerminalTab } from "./terminal-tab";
interface Props {
+33 -11
View File
@@ -30,22 +30,30 @@ import { MenuItem } from "../menu";
import { MenuActions } from "../menu/menu-actions";
import { ResizeDirection, ResizingAnchor } from "../resizing-anchor";
import { CreateResource } from "./create-resource";
import { createResourceTab } from "./create-resource.store";
import { DockTabs } from "./dock-tabs";
import { dockStore, DockTab, TabKind } from "./dock.store";
import { DockStore, DockTab, TabKind } from "./dock-store/dock.store";
import { EditResource } from "./edit-resource";
import { InstallChart } from "./install-chart";
import { Logs } from "./logs";
import { TerminalWindow } from "./terminal-window";
import { createTerminalTab } from "./terminal.store";
import { UpgradeChart } from "./upgrade-chart";
import { withInjectables } from "@ogre-tools/injectable-react";
import createResourceTabInjectable from "./create-resource-tab/create-resource-tab.injectable";
import dockStoreInjectable from "./dock-store/dock-store.injectable";
import createTerminalTabInjectable from "./create-terminal-tab/create-terminal-tab.injectable";
interface Props {
className?: string;
}
interface Dependencies {
createResourceTab: () => void
createTerminalTab: () => void
dockStore: DockStore
}
@observer
export class Dock extends React.Component<Props> {
class NonInjectedDock extends React.Component<Props & Dependencies> {
private element = React.createRef<HTMLDivElement>();
componentDidMount() {
@@ -57,7 +65,7 @@ export class Dock extends React.Component<Props> {
}
onKeyDown = (evt: KeyboardEvent) => {
const { close, selectedTab, closeTab } = dockStore;
const { close, selectedTab, closeTab } = this.props.dockStore;
const { code, ctrlKey, metaKey, shiftKey } = evt;
// Determine if user working inside <Dock/> or using any other areas in app
const dockIsFocused = this.element?.current.contains(document.activeElement);
@@ -75,7 +83,7 @@ export class Dock extends React.Component<Props> {
};
onChangeTab = (tab: DockTab) => {
const { open, selectTab } = dockStore;
const { open, selectTab } = this.props.dockStore;
open();
selectTab(tab.id);
@@ -100,7 +108,7 @@ export class Dock extends React.Component<Props> {
}
renderTabContent() {
const { isOpen, height, selectedTab } = dockStore;
const { isOpen, height, selectedTab } = this.props.dockStore;
if (!isOpen || !selectedTab) return null;
@@ -112,8 +120,8 @@ export class Dock extends React.Component<Props> {
}
render() {
const { className } = this.props;
const { isOpen, toggle, tabs, toggleFillSize, selectedTab, hasTabs, fullSize } = dockStore;
const { className, dockStore } = this.props;
const { isOpen, toggle, tabs, toggleFillSize, selectedTab, hasTabs, fullSize } = this.props.dockStore;
return (
<div
@@ -142,11 +150,11 @@ export class Dock extends React.Component<Props> {
<div className="toolbar flex gaps align-center box grow">
<div className="dock-menu box grow">
<MenuActions usePortal triggerIcon={{ material: "add", className: "new-dock-tab", tooltip: "New tab" }} closeOnScroll={false}>
<MenuItem className="create-terminal-tab" onClick={() => createTerminalTab()}>
<MenuItem className="create-terminal-tab" onClick={() => this.props.createTerminalTab()}>
<Icon small svg="terminal" size={15} />
Terminal session
</MenuItem>
<MenuItem className="create-resource-tab" onClick={() => createResourceTab()}>
<MenuItem className="create-resource-tab" onClick={() => this.props.createResourceTab()}>
<Icon small material="create" />
Create resource
</MenuItem>
@@ -173,3 +181,17 @@ export class Dock extends React.Component<Props> {
);
}
}
export const Dock = withInjectables<Dependencies, Props>(
NonInjectedDock,
{
getProps: (di, props) => ({
createResourceTab: di.inject(createResourceTabInjectable),
dockStore: di.inject(dockStoreInjectable),
createTerminalTab: di.inject(createTerminalTabInjectable),
...props,
}),
},
);
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import { EditResourceStore } from "./edit-resource.store";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const editResourceStoreInjectable = getInjectable({
instantiate: (di) =>
new EditResourceStore({
dockStore: di.inject(dockStoreInjectable),
createStorage: di.inject(createStorageInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default editResourceStoreInjectable;
@@ -19,13 +19,13 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { autoBind, noop } from "../../utils";
import { DockTabStore } from "./dock-tab.store";
import { autoBind, noop, StorageHelper } from "../../../utils";
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
import { autorun, IReactionDisposer } from "mobx";
import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store";
import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { apiManager } from "../../../common/k8s-api/api-manager";
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
import type { DockStore, DockTab, TabId } from "../dock-store/dock.store";
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
import { apiManager } from "../../../../common/k8s-api/api-manager";
import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store";
export interface EditingResource {
resource: string; // resource path, e.g. /api/v1/namespaces/default
@@ -33,13 +33,19 @@ export interface EditingResource {
firstDraft?: string;
}
interface Dependencies {
dockStore: DockStore
createStorage:<T> (storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
}
export class EditResourceStore extends DockTabStore<EditingResource> {
private watchers = new Map<TabId, IReactionDisposer>();
constructor() {
super({
constructor(protected dependencies: Dependencies) {
super(dependencies, {
storageKey: "edit_resource_store",
});
autoBind(this);
}
@@ -56,7 +62,7 @@ export class EditResourceStore extends DockTabStore<EditingResource> {
const store = apiManager.getStore(resource);
if (store) {
const isActiveTab = dockStore.isOpen && dockStore.selectedTabId === tabId;
const isActiveTab = this.dependencies.dockStore.isOpen && this.dependencies.dockStore.selectedTabId === tabId;
const obj = store.getByPath(resource);
// preload resource for editing
@@ -65,7 +71,7 @@ export class EditResourceStore extends DockTabStore<EditingResource> {
}
// auto-close tab when resource removed from store
else if (!obj && store.isLoaded) {
dockStore.closeTab(tabId);
this.dependencies.dockStore.closeTab(tabId);
}
}
}, {
@@ -102,7 +108,7 @@ export class EditResourceStore extends DockTabStore<EditingResource> {
return object.selfLink === resource;
}) || [];
return dockStore.getTabById(tabId);
return this.dependencies.dockStore.getTabById(tabId);
}
clearInitialDraft(tabId: TabId): void {
@@ -117,29 +123,3 @@ export class EditResourceStore extends DockTabStore<EditingResource> {
});
}
}
export const editResourceStore = new EditResourceStore();
export function editResourceTab(object: KubeObject, tabParams: DockTabCreateSpecific = {}) {
// use existing tab if already opened
let tab = editResourceStore.getTabByResource(object);
if (tab) {
dockStore.open();
dockStore.selectTab(tab.id);
}
// or create new tab
if (!tab) {
tab = dockStore.createTab({
title: `${object.kind}: ${object.getName()}`,
...tabParams,
kind: TabKind.EDIT_RESOURCE,
}, false);
editResourceStore.setData(tab.id, {
resource: object.selfLink,
});
}
return tab;
}
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { editResourceTab } from "./edit-resource-tab";
import editResourceStoreInjectable from "../edit-resource-store/edit-resource-store.injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
const editResourceTabInjectable = getInjectable({
instantiate: (di) => editResourceTab({
dockStore: di.inject(dockStoreInjectable),
editResourceStore: di.inject(editResourceStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default editResourceTabInjectable;
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store";
import type { EditResourceStore } from "../edit-resource-store/edit-resource.store";
interface Dependencies {
dockStore: DockStore;
editResourceStore: EditResourceStore;
}
export const editResourceTab =
({ dockStore, editResourceStore }: Dependencies) =>
(object: KubeObject, tabParams: DockTabCreateSpecific = {}) => {
// use existing tab if already opened
let tab = editResourceStore.getTabByResource(object);
if (tab) {
dockStore.open();
dockStore.selectTab(tab.id);
}
// or create new tab
if (!tab) {
tab = dockStore.createTab(
{
title: `${object.kind}: ${object.getName()}`,
...tabParams,
kind: TabKind.EDIT_RESOURCE,
},
false,
);
editResourceStore.setData(tab.id, {
resource: object.selfLink,
});
}
return tab;
};
+28 -11
View File
@@ -25,24 +25,30 @@ import React from "react";
import { computed, makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
import yaml from "js-yaml";
import type { DockTab } from "./dock.store";
import { editResourceStore } from "./edit-resource.store";
import type { DockTab } from "./dock-store/dock.store";
import type { EditResourceStore } from "./edit-resource-store/edit-resource.store";
import { InfoPanel } from "./info-panel";
import { Badge } from "../badge";
import { EditorPanel } from "./editor-panel";
import { Spinner } from "../spinner";
import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { createPatch } from "rfc6902";
import { withInjectables } from "@ogre-tools/injectable-react";
import editResourceStoreInjectable from "./edit-resource-store/edit-resource-store.injectable";
interface Props {
tab: DockTab;
}
interface Dependencies {
editResourceStore: EditResourceStore
}
@observer
export class EditResource extends React.Component<Props> {
class NonInjectedEditResource extends React.Component<Props & Dependencies> {
@observable error = "";
constructor(props: Props) {
constructor(props: Props & Dependencies) {
super(props);
makeObservable(this);
}
@@ -52,11 +58,11 @@ export class EditResource extends React.Component<Props> {
}
get isReadyForEditing() {
return editResourceStore.isReady(this.tabId);
return this.props.editResourceStore.isReady(this.tabId);
}
get resource(): KubeObject | undefined {
return editResourceStore.getResource(this.tabId);
return this.props.editResourceStore.getResource(this.tabId);
}
@computed get draft(): string {
@@ -64,7 +70,7 @@ export class EditResource extends React.Component<Props> {
return ""; // wait until tab's data and kube-object resource are loaded
}
const editData = editResourceStore.getData(this.tabId);
const editData = this.props.editResourceStore.getData(this.tabId);
if (typeof editData.draft === "string") {
return editData.draft;
@@ -76,7 +82,7 @@ export class EditResource extends React.Component<Props> {
}
saveDraft(draft: string) {
editResourceStore.getData(this.tabId).draft = draft;
this.props.editResourceStore.getData(this.tabId).draft = draft;
}
onChange = (draft: string) => {
@@ -93,13 +99,13 @@ export class EditResource extends React.Component<Props> {
return null;
}
const store = editResourceStore.getStore(this.tabId);
const store = this.props.editResourceStore.getStore(this.tabId);
const currentVersion = yaml.load(this.draft);
const firstVersion = yaml.load(editResourceStore.getData(this.tabId).firstDraft ?? this.draft);
const firstVersion = yaml.load(this.props.editResourceStore.getData(this.tabId).firstDraft ?? this.draft);
const patches = createPatch(firstVersion, currentVersion);
const updatedResource = await store.patch(this.resource, patches);
editResourceStore.clearInitialDraft(this.tabId);
this.props.editResourceStore.clearInitialDraft(this.tabId);
return (
<p>
@@ -141,3 +147,14 @@ export class EditResource extends React.Component<Props> {
);
}
}
export const EditResource = withInjectables<Dependencies, Props>(
NonInjectedEditResource,
{
getProps: (di, props) => ({
editResourceStore: di.inject(editResourceStoreInjectable),
...props,
}),
},
);
+22 -5
View File
@@ -24,9 +24,11 @@ import throttle from "lodash/throttle";
import React from "react";
import { makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { dockStore, TabId } from "./dock.store";
import type { DockStore, TabId } from "./dock-store/dock.store";
import { cssNames } from "../../utils";
import { MonacoEditor, MonacoEditorProps } from "../monaco-editor";
import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock-store/dock-store.injectable";
export interface EditorPanelProps {
tabId: TabId;
@@ -37,17 +39,21 @@ export interface EditorPanelProps {
onError?: MonacoEditorProps["onError"];
}
interface Dependencies {
dockStore: DockStore
}
const defaultProps: Partial<EditorPanelProps> = {
autoFocus: true,
};
@observer
export class EditorPanel extends React.Component<EditorPanelProps> {
class NonInjectedEditorPanel extends React.Component<EditorPanelProps & Dependencies> {
static defaultProps = defaultProps as object;
@observable.ref editor?: MonacoEditor;
constructor(props: EditorPanelProps) {
constructor(props: EditorPanelProps & Dependencies) {
super(props);
makeObservable(this);
}
@@ -55,12 +61,12 @@ export class EditorPanel extends React.Component<EditorPanelProps> {
componentDidMount() {
disposeOnUnmount(this, [
// keep focus on editor's area when <Dock/> just opened
reaction(() => dockStore.isOpen, isOpen => isOpen && this.editor?.focus(), {
reaction(() => this.props.dockStore.isOpen, isOpen => isOpen && this.editor?.focus(), {
fireImmediately: true,
}),
// focus to editor on dock's resize or turning into fullscreen mode
dockStore.onResize(throttle(() => this.editor?.focus(), 250)),
this.props.dockStore.onResize(throttle(() => this.editor?.focus(), 250)),
]);
}
@@ -82,3 +88,14 @@ export class EditorPanel extends React.Component<EditorPanelProps> {
);
}
}
export const EditorPanel = withInjectables<Dependencies, EditorPanelProps>(
NonInjectedEditorPanel,
{
getProps: (di, props) => ({
dockStore: di.inject(dockStoreInjectable),
...props,
}),
},
);
+21 -4
View File
@@ -28,8 +28,10 @@ import { cssNames } from "../../utils";
import { Button } from "../button";
import { Icon } from "../icon";
import { Spinner } from "../spinner";
import { dockStore, TabId } from "./dock.store";
import type { DockStore, TabId } from "./dock-store/dock.store";
import { Notifications } from "../notifications";
import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock-store/dock-store.injectable";
interface Props extends OptionalProps {
tabId: TabId;
@@ -50,8 +52,12 @@ interface OptionalProps {
showStatusPanel?: boolean;
}
interface Dependencies {
dockStore: DockStore
}
@observer
export class InfoPanel extends Component<Props> {
class NonInjectedInfoPanel extends Component<Props & Dependencies> {
static defaultProps: OptionalProps = {
submitLabel: "Submit",
submittingMessage: "Submitting..",
@@ -65,7 +71,7 @@ export class InfoPanel extends Component<Props> {
@observable error = "";
@observable waiting = false;
constructor(props: Props) {
constructor(props: Props & Dependencies) {
super(props);
makeObservable(this);
}
@@ -104,7 +110,7 @@ export class InfoPanel extends Component<Props> {
};
close = () => {
dockStore.closeTab(this.props.tabId);
this.props.dockStore.closeTab(this.props.tabId);
};
renderErrorIcon() {
@@ -159,3 +165,14 @@ export class InfoPanel extends Component<Props> {
);
}
}
export const InfoPanel = withInjectables<Dependencies, Props>(
NonInjectedInfoPanel,
{
getProps: (di, props) => ({
dockStore: di.inject(dockStoreInjectable),
...props,
}),
},
);
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { InstallChartStore } from "./install-chart.store";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable";
import type { IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const installChartStoreInjectable = getInjectable({
instantiate: (di) => {
const createDockTabStore = di.inject(createDockTabStoreInjectable);
return new InstallChartStore({
dockStore: di.inject(dockStoreInjectable),
createStorage: di.inject(createStorageInjectable),
versionsStore: createDockTabStore<string[]>(),
detailsStore: createDockTabStore<IReleaseUpdateDetails>(),
});
},
lifecycle: lifecycleEnum.singleton,
});
export default installChartStoreInjectable;
@@ -20,11 +20,12 @@
*/
import { action, autorun, makeObservable } from "mobx";
import { dockStore, DockTabCreateSpecific, TabId, TabKind } from "./dock.store";
import { DockTabStore } from "./dock-tab.store";
import { getChartDetails, getChartValues, HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
import type { IReleaseUpdateDetails } from "../../../common/k8s-api/endpoints/helm-releases.api";
import { Notifications } from "../notifications";
import { DockStore, TabId, TabKind } from "../dock-store/dock.store";
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
import { getChartDetails, getChartValues } from "../../../../common/k8s-api/endpoints/helm-charts.api";
import type { IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import { Notifications } from "../../notifications";
import type { StorageHelper } from "../../../utils";
export interface IChartInstallData {
name: string;
@@ -37,17 +38,24 @@ export interface IChartInstallData {
lastVersion?: boolean;
}
export class InstallChartStore extends DockTabStore<IChartInstallData> {
public versions = new DockTabStore<string[]>();
public details = new DockTabStore<IReleaseUpdateDetails>();
interface Dependencies {
dockStore: DockStore,
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
constructor() {
super({
storageKey: "install_charts",
});
versionsStore: DockTabStore<string[]>,
detailsStore: DockTabStore<IReleaseUpdateDetails>
}
export class InstallChartStore extends DockTabStore<IChartInstallData> {
constructor(protected dependencies: Dependencies) {
super(
dependencies,
{ storageKey: "install_charts" },
);
makeObservable(this);
autorun(() => {
const { selectedTab, isOpen } = dockStore;
const { selectedTab, isOpen } = dependencies.dockStore;
if (selectedTab?.kind === TabKind.INSTALL_CHART && isOpen) {
this.loadData(selectedTab.id)
@@ -56,6 +64,14 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
}, { delay: 250 });
}
get versions() {
return this.dependencies.versionsStore;
}
get details() {
return this.dependencies.detailsStore;
}
@action
async loadData(tabId: string) {
const promises = [];
@@ -99,25 +115,3 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
super.setData(tabId, data);
}
}
export const installChartStore = new InstallChartStore();
export function createInstallChartTab(chart: HelmChart, tabParams: DockTabCreateSpecific = {}) {
const { name, repo, version } = chart;
const tab = dockStore.createTab({
title: `Helm Install: ${repo}/${name}`,
...tabParams,
kind: TabKind.INSTALL_CHART,
}, false);
installChartStore.setData(tab.id, {
name,
repo,
version,
namespace: "default",
releaseName: "",
description: "",
});
return tab;
}
+39 -13
View File
@@ -24,39 +24,52 @@ import "./install-chart.scss";
import React, { Component } from "react";
import { action, makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
import { dockStore, DockTab } from "./dock.store";
import type { DockStore, DockTab } from "./dock-store/dock.store";
import { InfoPanel } from "./info-panel";
import { Badge } from "../badge";
import { NamespaceSelect } from "../+namespaces/namespace-select";
import { prevDefault } from "../../utils";
import { IChartInstallData, installChartStore } from "./install-chart.store";
import type { IChartInstallData, InstallChartStore } from "./install-chart-store/install-chart.store";
import { Spinner } from "../spinner";
import { Icon } from "../icon";
import { Button } from "../button";
import { releaseStore } from "../+apps-releases/release.store";
import { LogsDialog } from "../dialog/logs-dialog";
import { Select, SelectOption } from "../select";
import { Input } from "../input";
import { EditorPanel } from "./editor-panel";
import { navigate } from "../../navigation";
import { releaseURL } from "../../../common/routes";
import type {
IReleaseCreatePayload,
IReleaseUpdateDetails,
} from "../../../common/k8s-api/endpoints/helm-releases.api";
import releaseStoreInjectable from "../+apps-releases/release-store.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import installChartStoreInjectable from "./install-chart-store/install-chart-store.injectable";
import dockStoreInjectable from "./dock-store/dock-store.injectable";
interface Props {
tab: DockTab;
}
interface Dependencies {
createRelease: (payload: IReleaseCreatePayload) => Promise<IReleaseUpdateDetails>
installChartStore: InstallChartStore
dockStore: DockStore
}
@observer
export class InstallChart extends Component<Props> {
class NonInjectedInstallChart extends Component<Props & Dependencies> {
@observable error = "";
@observable showNotes = false;
constructor(props: Props) {
constructor(props: Props & Dependencies) {
super(props);
makeObservable(this);
}
get chartData() {
return installChartStore.getData(this.tabId);
return this.props.installChartStore.getData(this.tabId);
}
get tabId() {
@@ -64,11 +77,11 @@ export class InstallChart extends Component<Props> {
}
get versions() {
return installChartStore.versions.getData(this.tabId);
return this.props.installChartStore.versions.getData(this.tabId);
}
get releaseDetails() {
return installChartStore.details.getData(this.tabId);
return this.props.installChartStore.details.getData(this.tabId);
}
viewRelease = () => {
@@ -80,20 +93,20 @@ export class InstallChart extends Component<Props> {
namespace: release.namespace,
},
}));
dockStore.closeTab(this.tabId);
this.props.dockStore.closeTab(this.tabId);
};
save(data: Partial<IChartInstallData>) {
const chart = { ...this.chartData, ...data };
installChartStore.setData(this.tabId, chart);
this.props.installChartStore.setData(this.tabId, chart);
}
onVersionChange = (option: SelectOption) => {
const version = option.value;
this.save({ version, values: "" });
installChartStore.loadValues(this.tabId);
this.props.installChartStore.loadValues(this.tabId);
};
@action
@@ -117,13 +130,13 @@ export class InstallChart extends Component<Props> {
install = async () => {
const { repo, name, version, namespace, values, releaseName } = this.chartData;
const details = await releaseStore.create({
const details = await this.props.createRelease({
name: releaseName || undefined,
chart: name,
repo, namespace, version, values,
});
installChartStore.details.setData(this.tabId, details);
this.props.installChartStore.details.setData(this.tabId, details);
return (
<p>Chart Release <b>{details.release.name}</b> successfully created.</p>
@@ -219,3 +232,16 @@ export class InstallChart extends Component<Props> {
);
}
}
export const InstallChart = withInjectables<Dependencies, Props>(
NonInjectedInstallChart,
{
getProps: (di, props) => ({
createRelease: di.inject(releaseStoreInjectable).create,
installChartStore: di.inject(installChartStoreInjectable),
dockStore: di.inject(dockStoreInjectable),
...props,
}),
},
);
+22 -4
View File
@@ -26,10 +26,12 @@ import { observer } from "mobx-react";
import { Pod } from "../../../common/k8s-api/endpoints";
import { cssNames, saveFileDialog } from "../../utils";
import { logStore } from "./log.store";
import { Checkbox } from "../checkbox";
import { Icon } from "../icon";
import type { LogTabData } from "./log-tab.store";
import type { LogTabData } from "./log-tab-store/log-tab.store";
import type { LogStore } from "./log-store/log.store";
import { withInjectables } from "@ogre-tools/injectable-react";
import logStoreInjectable from "./log-store/log-store.injectable";
interface Props {
tabData?: LogTabData
@@ -38,8 +40,12 @@ interface Props {
reload: () => void
}
export const LogControls = observer((props: Props) => {
const { tabData, save, reload, logs } = props;
interface Dependencies {
logStore: LogStore
}
const NonInjectedLogControls = observer((props: Props & Dependencies) => {
const { tabData, save, reload, logs, logStore } = props;
if (!tabData) {
return null;
@@ -98,3 +104,15 @@ export const LogControls = observer((props: Props) => {
</div>
);
});
export const LogControls = withInjectables<Dependencies, Props>(
NonInjectedLogControls,
{
getProps: (di, props) => ({
logStore: di.inject(logStoreInjectable),
...props,
}),
},
);
+33 -10
View File
@@ -29,15 +29,18 @@ import { action, computed, observable, makeObservable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import moment from "moment-timezone";
import type { Align, ListOnScrollProps } from "react-window";
import { SearchStore, searchStore } from "../../../common/search-store";
import { SearchStore } from "../../search-store/search-store";
import { UserStore } from "../../../common/user-store";
import { array, boundMethod, cssNames } from "../../utils";
import { Spinner } from "../spinner";
import { VirtualList } from "../virtual-list";
import { logStore } from "./log.store";
import { logTabStore } from "./log-tab.store";
import type { LogStore } from "./log-store/log.store";
import type { LogTabStore } from "./log-tab-store/log-tab.store";
import { ToBottom } from "./to-bottom";
import { withInjectables } from "@ogre-tools/injectable-react";
import logTabStoreInjectable from "./log-tab-store/log-tab-store.injectable";
import logStoreInjectable from "./log-store/log-store.injectable";
import searchStoreInjectable from "../../search-store/search-store.injectable";
interface Props {
logs: string[]
@@ -48,8 +51,14 @@ interface Props {
const colorConverter = new AnsiUp();
interface Dependencies {
logTabStore: LogTabStore
logStore: LogStore
searchStore: SearchStore
}
@observer
export class LogList extends React.Component<Props> {
class NonInjectedLogList extends React.Component<Props & Dependencies> {
@observable isJumpButtonVisible = false;
@observable isLastLineVisible = true;
@@ -57,7 +66,7 @@ export class LogList extends React.Component<Props> {
private virtualListRef = React.createRef<VirtualList>(); // A reference for VirtualList component
private lineHeight = 18; // Height of a log line. Should correlate with styles in pod-log-list.scss
constructor(props: Props) {
constructor(props: Props & Dependencies) {
super(props);
makeObservable(this);
}
@@ -108,14 +117,14 @@ export class LogList extends React.Component<Props> {
*/
@computed
get logs() {
const showTimestamps = logTabStore.getData(this.props.id)?.showTimestamps;
const showTimestamps = this.props.logTabStore.getData(this.props.id)?.showTimestamps;
if (!showTimestamps) {
return logStore.logsWithoutTimestamps;
return this.props.logStore.logsWithoutTimestamps;
}
return this.props.logs
.map(log => logStore.splitOutTimestamp(log))
.map(log => this.props.logStore.splitOutTimestamp(log))
.map(([logTimestamp, log]) => (`${logTimestamp && moment.tz(logTimestamp, UserStore.getInstance().localeTimezone).format()}${log}`));
}
@@ -187,7 +196,7 @@ export class LogList extends React.Component<Props> {
* @returns A react element with a row itself
*/
getLogRow = (rowIndex: number) => {
const { searchQuery, isActiveOverlay } = searchStore;
const { searchQuery, isActiveOverlay } = this.props.searchStore;
const item = this.logs[rowIndex];
const contents: React.ReactElement[] = [];
const ansiToHtml = (ansi: string) => DOMPurify.sanitize(colorConverter.ansi_to_html(ansi));
@@ -270,3 +279,17 @@ export class LogList extends React.Component<Props> {
);
}
}
export const LogList = withInjectables<Dependencies, Props>(
NonInjectedLogList,
{
getProps: (di, props) => ({
logTabStore: di.inject(logTabStoreInjectable),
logStore: di.inject(logStoreInjectable),
searchStore: di.inject(searchStoreInjectable),
...props,
}),
},
);
@@ -27,9 +27,11 @@ import { observer } from "mobx-react";
import { Pod } from "../../../common/k8s-api/endpoints";
import { Badge } from "../badge";
import { Select, SelectOption } from "../select";
import { LogTabData, logTabStore } from "./log-tab.store";
import type { LogTabData, LogTabStore } from "./log-tab-store/log-tab.store";
import { podsStore } from "../+workloads-pods/pods.store";
import type { TabId } from "./dock.store";
import type { TabId } from "./dock-store/dock.store";
import logTabStoreInjectable from "./log-tab-store/log-tab-store.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
interface Props {
tabId: TabId
@@ -38,8 +40,12 @@ interface Props {
reload: () => void
}
export const LogResourceSelector = observer((props: Props) => {
const { tabData, save, reload, tabId } = props;
interface Dependencies {
logTabStore: LogTabStore
}
const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) => {
const { tabData, save, reload, tabId, logTabStore } = props;
const { selectedPod, selectedContainer, pods } = tabData;
const pod = new Pod(selectedPod);
const containers = pod.getContainers();
@@ -58,6 +64,7 @@ export const LogResourceSelector = observer((props: Props) => {
const selectedPod = podsStore.getByName(option.value, pod.getNs());
save({ selectedPod });
logTabStore.renameTab(tabId);
};
@@ -114,3 +121,15 @@ export const LogResourceSelector = observer((props: Props) => {
</div>
);
});
export const LogResourceSelector = withInjectables<Dependencies, Props>(
NonInjectedLogResourceSelector,
{
getProps: (di, props) => ({
logTabStore: di.inject(logTabStoreInjectable),
...props,
}),
},
);
+20 -3
View File
@@ -24,8 +24,10 @@ import "./log-search.scss";
import React, { useEffect } from "react";
import { observer } from "mobx-react";
import { SearchInput } from "../input";
import { searchStore } from "../../../common/search-store";
import type { SearchStore } from "../../search-store/search-store";
import { Icon } from "../icon";
import { withInjectables } from "@ogre-tools/injectable-react";
import searchStoreInjectable from "../../search-store/search-store.injectable";
export interface PodLogSearchProps {
onSearch: (query: string) => void
@@ -37,8 +39,12 @@ interface Props extends PodLogSearchProps {
logs: string[]
}
export const LogSearch = observer((props: Props) => {
const { logs, onSearch, toPrevOverlay, toNextOverlay } = props;
interface Dependencies {
searchStore: SearchStore
}
const NonInjectedLogSearch = observer((props: Props & Dependencies) => {
const { logs, onSearch, toPrevOverlay, toNextOverlay, searchStore } = props;
const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore;
const jumpDisabled = !searchQuery || !occurrences.length;
const findCounts = (
@@ -102,3 +108,14 @@ export const LogSearch = observer((props: Props) => {
</div>
);
});
export const LogSearch = withInjectables<Dependencies, Props>(
NonInjectedLogSearch,
{
getProps: (di, props) => ({
searchStore: di.inject(searchStoreInjectable),
...props,
}),
},
);
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { LogStore } from "./log.store";
import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
const logStoreInjectable = getInjectable({
instantiate: (di) => new LogStore({
logTabStore: di.inject(logTabStoreInjectable),
dockStore: di.inject(dockStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default logStoreInjectable;
@@ -21,18 +21,23 @@
import { autorun, computed, observable, makeObservable } from "mobx";
import { IPodLogsQuery, Pod, podsApi } from "../../../common/k8s-api/endpoints";
import { autoBind, interval } from "../../utils";
import { dockStore, TabId, TabKind } from "./dock.store";
import { logTabStore } from "./log-tab.store";
import { IPodLogsQuery, Pod, podsApi } from "../../../../common/k8s-api/endpoints";
import { autoBind, interval } from "../../../utils";
import { DockStore, TabId, TabKind } from "../dock-store/dock.store";
import type { LogTabStore } from "../log-tab-store/log-tab.store";
type PodLogLine = string;
const logLinesToLoad = 500;
interface Dependencies {
logTabStore: LogTabStore
dockStore: DockStore
}
export class LogStore {
private refresher = interval(10, () => {
const id = dockStore.selectedTabId;
const id = this.dependencies.dockStore.selectedTabId;
if (!this.podLogs.get(id)) return;
this.loadMore(id);
@@ -40,12 +45,12 @@ export class LogStore {
@observable podLogs = observable.map<TabId, PodLogLine[]>();
constructor() {
constructor(private dependencies: Dependencies) {
makeObservable(this);
autoBind(this);
autorun(() => {
const { selectedTab, isOpen } = dockStore;
const { selectedTab, isOpen } = this.dependencies.dockStore;
if (selectedTab?.kind === TabKind.POD_LOGS && isOpen) {
this.refresher.start();
@@ -121,7 +126,7 @@ export class LogStore {
* @returns A fetch request promise
*/
async loadLogs(tabId: TabId, params: Partial<IPodLogsQuery>): Promise<string[]> {
const data = logTabStore.getData(tabId);
const data = this.dependencies.logTabStore.getData(tabId);
const { selectedContainer, previous } = data;
const pod = new Pod(data.selectedPod);
const namespace = pod.getNs();
@@ -152,7 +157,7 @@ export class LogStore {
*/
@computed
get logs() {
return this.podLogs.get(dockStore.selectedTabId) ?? [];
return this.podLogs.get(this.dependencies.dockStore.selectedTabId) ?? [];
}
/**
@@ -201,5 +206,3 @@ export class LogStore {
this.podLogs.delete(tabId);
}
}
export const logStore = new LogStore();
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { LogTabStore } from "./log-tab.store";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const logTabStoreInjectable = getInjectable({
instantiate: (di) => new LogTabStore({
dockStore: di.inject(dockStoreInjectable),
createStorage: di.inject(createStorageInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default logTabStoreInjectable;
@@ -21,13 +21,14 @@
import uniqueId from "lodash/uniqueId";
import { reaction } from "mobx";
import { podsStore } from "../+workloads-pods/pods.store";
import { podsStore } from "../../+workloads-pods/pods.store";
import { IPodContainer, Pod } from "../../../common/k8s-api/endpoints";
import type { WorkloadKubeObject } from "../../../common/k8s-api/workload-kube-object";
import logger from "../../../common/logger";
import { DockTabStore } from "./dock-tab.store";
import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store";
import { IPodContainer, Pod } from "../../../../common/k8s-api/endpoints";
import type { WorkloadKubeObject } from "../../../../common/k8s-api/workload-kube-object";
import logger from "../../../../common/logger";
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store";
import type { StorageHelper } from "../../../utils";
export interface LogTabData {
pods: Pod[];
@@ -46,9 +47,14 @@ interface WorkloadLogsTabData {
workload: WorkloadKubeObject
}
interface Dependencies {
dockStore: DockStore
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
}
export class LogTabStore extends DockTabStore<LogTabData> {
constructor() {
super({
constructor(protected dependencies: Dependencies) {
super(dependencies, {
storageKey: "pod_logs",
});
@@ -86,11 +92,11 @@ export class LogTabStore extends DockTabStore<LogTabData> {
renameTab(tabId: string) {
const { selectedPod } = this.getData(tabId);
dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`);
this.dependencies.dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`);
}
private createDockTab(tabParams: DockTabCreateSpecific) {
dockStore.createTab({
this.dependencies.dockStore.createTab({
...tabParams,
kind: TabKind.POD_LOGS,
}, false);
@@ -143,8 +149,7 @@ export class LogTabStore extends DockTabStore<LogTabData> {
private closeTab(tabId: string) {
this.clearData(tabId);
dockStore.closeTab(tabId);
this.dependencies.dockStore.closeTab(tabId);
}
}
export const logTabStore = new LogTabStore();
+43 -16
View File
@@ -23,29 +23,39 @@ import React from "react";
import { observable, reaction, makeObservable } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { searchStore } from "../../../common/search-store";
import { boundMethod } from "../../utils";
import type { DockTab } from "./dock.store";
import type { DockTab } from "./dock-store/dock.store";
import { InfoPanel } from "./info-panel";
import { LogResourceSelector } from "./log-resource-selector";
import { LogList } from "./log-list";
import { logStore } from "./log.store";
import type { LogStore } from "./log-store/log.store";
import { LogSearch } from "./log-search";
import { LogControls } from "./log-controls";
import { LogTabData, logTabStore } from "./log-tab.store";
import type { LogTabData, LogTabStore } from "./log-tab-store/log-tab.store";
import { withInjectables } from "@ogre-tools/injectable-react";
import logTabStoreInjectable from "./log-tab-store/log-tab-store.injectable";
import logStoreInjectable from "./log-store/log-store.injectable";
import type { SearchStore } from "../../search-store/search-store";
import searchStoreInjectable from "../../search-store/search-store.injectable";
interface Props {
className?: string
tab: DockTab
}
interface Dependencies {
logTabStore: LogTabStore
logStore: LogStore
searchStore: SearchStore
}
@observer
export class Logs extends React.Component<Props> {
class NonInjectedLogs extends React.Component<Props & Dependencies> {
@observable isLoading = true;
private logListElement = React.createRef<LogList>(); // A reference for VirtualList component
private logListElement = React.createRef<typeof LogList>(); // A reference for VirtualList component
constructor(props: Props) {
constructor(props: Props & Dependencies) {
super(props);
makeObservable(this);
}
@@ -62,12 +72,12 @@ export class Logs extends React.Component<Props> {
load = async () => {
this.isLoading = true;
await logStore.load(this.tabId);
await this.props.logStore.load(this.tabId);
this.isLoading = false;
};
reload = async () => {
logStore.clearLogs(this.tabId);
this.props.logStore.clearLogs(this.tabId);
await this.load();
};
@@ -85,10 +95,12 @@ export class Logs extends React.Component<Props> {
*/
@boundMethod
toOverlay() {
const { activeOverlayLine } = searchStore;
const { activeOverlayLine } = this.props.searchStore;
if (!this.logListElement.current || activeOverlayLine === undefined) return;
// Scroll vertically
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.logListElement.current.scrollToItem(activeOverlayLine, "center");
// Scroll horizontally in timeout since virtual list need some time to prepare its contents
setTimeout(() => {
@@ -104,14 +116,14 @@ export class Logs extends React.Component<Props> {
return null;
}
const logs = logStore.logs;
const searchLogs = data.showTimestamps ? logs : logStore.logsWithoutTimestamps;
const logs = this.props.logStore.logs;
const searchLogs = data.showTimestamps ? logs : this.props.logStore.logsWithoutTimestamps;
const controls = (
<div className="flex gaps">
<LogResourceSelector
tabId={this.tabId}
tabData={data}
save={newData => logTabStore.setData(this.tabId, { ...data, ...newData })}
save={newData => this.props.logTabStore.setData(this.tabId, { ...data, ...newData })}
reload={this.reload}
/>
<LogSearch
@@ -135,8 +147,8 @@ export class Logs extends React.Component<Props> {
}
render() {
const logs = logStore.logs;
const data = logTabStore.getData(this.tabId);
const logs = this.props.logStore.logs;
const data = this.props.logTabStore.getData(this.tabId);
if (!data) {
this.reload();
@@ -150,15 +162,30 @@ export class Logs extends React.Component<Props> {
id={this.tabId}
isLoading={this.isLoading}
load={this.load}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
ref={this.logListElement}
/>
<LogControls
logs={logs}
tabData={data}
save={newData => logTabStore.setData(this.tabId, { ...data, ...newData })}
save={newData => this.props.logTabStore.setData(this.tabId, { ...data, ...newData })}
reload={this.reload}
/>
</div>
);
}
}
export const Logs = withInjectables<Dependencies, Props>(
NonInjectedLogs,
{
getProps: (di, props) => ({
logTabStore: di.inject(logTabStoreInjectable),
logStore: di.inject(logStoreInjectable),
searchStore: di.inject(searchStoreInjectable),
...props,
}),
},
);
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { TerminalStore } from "./terminal.store";
import createTerminalTabInjectable from "../create-terminal-tab/create-terminal-tab.injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import createTerminalInjectable from "../terminal/create-terminal.injectable";
const terminalStoreInjectable = getInjectable({
instantiate: (di) => new TerminalStore({
createTerminalTab: di.inject(createTerminalTabInjectable),
dockStore: di.inject(dockStoreInjectable),
createTerminal: di.inject(createTerminalInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default terminalStoreInjectable;
@@ -20,36 +20,39 @@
*/
import { autorun, observable, when } from "mobx";
import { autoBind, noop, Singleton } from "../../utils";
import { Terminal } from "./terminal";
import { TerminalApi, TerminalChannels } from "../../api/terminal-api";
import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store";
import { WebSocketApiState } from "../../api/websocket-api";
import { Notifications } from "../notifications";
import { autoBind, noop } from "../../../utils";
import type { Terminal } from "../terminal/terminal";
import { TerminalApi, TerminalChannels } from "../../../api/terminal-api";
import {
DockStore,
DockTab,
DockTabCreate,
TabId,
TabKind,
} from "../dock-store/dock.store";
import { WebSocketApiState } from "../../../api/websocket-api";
import { Notifications } from "../../notifications";
export interface ITerminalTab extends DockTab {
node?: string; // activate node shell mode
}
export function createTerminalTab(tabParams: DockTabCreateSpecific = {}) {
return dockStore.createTab({
title: `Terminal`,
...tabParams,
kind: TabKind.TERMINAL,
});
interface Dependencies {
createTerminalTab: () => DockTabCreate
dockStore: DockStore
createTerminal: (tabId: TabId, api: TerminalApi) => Terminal
}
export class TerminalStore extends Singleton {
export class TerminalStore {
protected terminals = new Map<TabId, Terminal>();
protected connections = observable.map<TabId, TerminalApi>();
constructor() {
super();
constructor(private dependencies: Dependencies) {
autoBind(this);
// connect active tab
autorun(() => {
const { selectedTab, isOpen } = dockStore;
const { selectedTab, isOpen } = dependencies.dockStore;
if (selectedTab?.kind === TabKind.TERMINAL && isOpen) {
this.connect(selectedTab.id);
@@ -57,7 +60,7 @@ export class TerminalStore extends Singleton {
});
// disconnect closed tabs
autorun(() => {
const currentTabs = dockStore.tabs.map(tab => tab.id);
const currentTabs = dependencies.dockStore.tabs.map(tab => tab.id);
for (const [tabId] of this.connections) {
if (!currentTabs.includes(tabId)) this.disconnect(tabId);
@@ -69,12 +72,12 @@ export class TerminalStore extends Singleton {
if (this.isConnected(tabId)) {
return;
}
const tab: ITerminalTab = dockStore.getTabById(tabId);
const tab: ITerminalTab = this.dependencies.dockStore.getTabById(tabId);
const api = new TerminalApi({
id: tabId,
node: tab.node,
});
const terminal = new Terminal(tabId, api);
const terminal = this.dependencies.createTerminal(tabId, api);
this.connections.set(tabId, api);
this.terminals.set(tabId, terminal);
@@ -111,11 +114,11 @@ export class TerminalStore extends Singleton {
const { enter, newTab, tabId } = options;
if (tabId) {
dockStore.selectTab(tabId);
this.dependencies.dockStore.selectTab(tabId);
}
if (newTab) {
const tab = createTerminalTab();
const tab = this.dependencies.createTerminalTab();
await when(() => this.connections.has(tab.id));
@@ -134,7 +137,7 @@ export class TerminalStore extends Singleton {
clearTimeout(notifyVeryLong);
}
const terminalApi = this.connections.get(dockStore.selectedTabId);
const terminalApi = this.connections.get(this.dependencies.dockStore.selectedTabId);
if (terminalApi) {
if (enter) {
@@ -146,7 +149,10 @@ export class TerminalStore extends Singleton {
data: command,
});
} else {
console.warn("The selected tab is does not have a connection. Cannot send command.", { tabId: dockStore.selectedTabId, command });
console.warn(
"The selected tab is does not have a connection. Cannot send command.",
{ tabId: this.dependencies.dockStore.selectedTabId, command },
);
}
}
@@ -160,25 +166,3 @@ export class TerminalStore extends Singleton {
});
}
}
/**
* @deprecated use `TerminalStore.getInstance()` instead
*/
export const terminalStore = new Proxy({}, {
get(target, p) {
if (p === "$$typeof") {
return "TerminalStore";
}
const ts = TerminalStore.getInstance();
const res = (ts as any)?.[p];
if (typeof res === "function") {
return function (...args: any[]) {
return res.apply(ts, args);
};
}
return res;
},
}) as TerminalStore;
+30 -7
View File
@@ -26,18 +26,26 @@ import { observer } from "mobx-react";
import { boundMethod, cssNames } from "../../utils";
import { DockTab, DockTabProps } from "./dock-tab";
import { Icon } from "../icon";
import { terminalStore } from "./terminal.store";
import { dockStore } from "./dock.store";
import type { TerminalStore } from "./terminal-store/terminal.store";
import type { DockStore } from "./dock-store/dock.store";
import { reaction } from "mobx";
import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock-store/dock-store.injectable";
import terminalStoreInjectable from "./terminal-store/terminal-store.injectable";
interface Props extends DockTabProps {
}
interface Dependencies {
dockStore: DockStore
terminalStore: TerminalStore
}
@observer
export class TerminalTab extends React.Component<Props> {
class NonInjectedTerminalTab extends React.Component<Props & Dependencies> {
componentDidMount() {
reaction(() => this.isDisconnected === true, () => {
dockStore.closeTab(this.tabId);
this.props.dockStore.closeTab(this.tabId);
});
}
@@ -46,12 +54,12 @@ export class TerminalTab extends React.Component<Props> {
}
get isDisconnected() {
return terminalStore.isDisconnected(this.tabId);
return this.props.terminalStore.isDisconnected(this.tabId);
}
@boundMethod
reconnect() {
terminalStore.reconnect(this.tabId);
this.props.terminalStore.reconnect(this.tabId);
}
render() {
@@ -60,9 +68,11 @@ export class TerminalTab extends React.Component<Props> {
disconnected: this.isDisconnected,
});
const { dockStore, terminalStore, ...tabProps } = this.props;
return (
<DockTab
{...this.props}
{...tabProps}
className={className}
icon={tabIcon}
moreActions={this.isDisconnected && (
@@ -78,3 +88,16 @@ export class TerminalTab extends React.Component<Props> {
);
}
}
export const TerminalTab = withInjectables<Dependencies, Props>(
NonInjectedTerminalTab,
{
getProps: (di, props) => ({
dockStore: di.inject(dockStoreInjectable),
terminalStore: di.inject(terminalStoreInjectable),
...props,
}),
},
);
@@ -24,29 +24,37 @@ import "./terminal-window.scss";
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
import { cssNames } from "../../utils";
import type { Terminal } from "./terminal";
import { TerminalStore } from "./terminal.store";
import type { Terminal } from "./terminal/terminal";
import type { TerminalStore } from "./terminal-store/terminal.store";
import { ThemeStore } from "../../theme.store";
import { dockStore, DockTab, TabKind, TabId } from "./dock.store";
import { DockTab, TabKind, TabId, DockStore } from "./dock-store/dock.store";
import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "./dock-store/dock-store.injectable";
import terminalStoreInjectable from "./terminal-store/terminal-store.injectable";
interface Props {
tab: DockTab;
}
interface Dependencies {
dockStore: DockStore
terminalStore: TerminalStore
}
@observer
export class TerminalWindow extends React.Component<Props> {
class NonInjectedTerminalWindow extends React.Component<Props & Dependencies> {
public elem: HTMLElement;
public terminal: Terminal;
componentDidMount() {
disposeOnUnmount(this, [
dockStore.onTabChange(({ tabId }) => this.activate(tabId), {
this.props.dockStore.onTabChange(({ tabId }) => this.activate(tabId), {
tabKind: TabKind.TERMINAL,
fireImmediately: true,
}),
// refresh terminal available space (cols/rows) when <Dock/> resized
dockStore.onResize(() => this.terminal?.fitLazy(), {
this.props.dockStore.onResize(() => this.terminal?.fitLazy(), {
fireImmediately: true,
}),
]);
@@ -54,7 +62,7 @@ export class TerminalWindow extends React.Component<Props> {
activate(tabId: TabId) {
this.terminal?.detach(); // detach previous
this.terminal = TerminalStore.getInstance().getTerminal(tabId);
this.terminal = this.props.terminalStore.getTerminal(tabId);
this.terminal.attachTo(this.elem);
}
@@ -67,3 +75,16 @@ export class TerminalWindow extends React.Component<Props> {
);
}
}
export const TerminalWindow = withInjectables<Dependencies, Props>(
NonInjectedTerminalWindow,
{
getProps: (di, props) => ({
dockStore: di.inject(dockStoreInjectable),
terminalStore: di.inject(terminalStoreInjectable),
...props,
}),
},
);
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { Terminal } from "./terminal";
import type { TabId } from "../dock-store/dock.store";
import type { TerminalApi } from "../../../api/terminal-api";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
const createTerminalInjectable = getInjectable({
instantiate: (di) => {
const dependencies = {
dockStore: di.inject(dockStoreInjectable),
};
return (tabId: TabId, api: TerminalApi) =>
new Terminal(dependencies, tabId, api);
},
lifecycle: lifecycleEnum.singleton,
});
export default createTerminalInjectable;
@@ -23,15 +23,19 @@ import debounce from "lodash/debounce";
import { reaction } from "mobx";
import { Terminal as XTerm } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import { dockStore, TabId } from "./dock.store";
import { TerminalApi, TerminalChannels } from "../../api/terminal-api";
import { ThemeStore } from "../../theme.store";
import { boundMethod, disposer } from "../../utils";
import { isMac } from "../../../common/vars";
import type { DockStore, TabId } from "../dock-store/dock.store";
import { TerminalApi, TerminalChannels } from "../../../api/terminal-api";
import { ThemeStore } from "../../../theme.store";
import { boundMethod, disposer } from "../../../utils";
import { isMac } from "../../../../common/vars";
import { camelCase, once } from "lodash";
import { UserStore } from "../../../common/user-store";
import { UserStore } from "../../../../common/user-store";
import { clipboard } from "electron";
import logger from "../../../common/logger";
import logger from "../../../../common/logger";
interface Dependencies {
dockStore: DockStore
}
export class Terminal {
public static readonly spawningPool = (() => {
@@ -47,7 +51,7 @@ export class Terminal {
})();
static async preloadFonts() {
const fontPath = require("../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires
const fontPath = require("../../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires
const fontFace = new FontFace("RobotoMono", `url(${fontPath})`);
await fontFace.load();
@@ -90,7 +94,7 @@ export class Terminal {
}
get isActive() {
const { isOpen, selectedTabId } = dockStore;
const { isOpen, selectedTabId } = this.dependencies.dockStore;
return isOpen && selectedTabId === this.tabId;
}
@@ -108,7 +112,7 @@ export class Terminal {
}
}
constructor(public tabId: TabId, protected api: TerminalApi) {
constructor(private dependencies: Dependencies, public tabId: TabId, protected api: TerminalApi) {
// enable terminal addons
this.xterm.loadAddon(this.fitAddon);
@@ -132,7 +136,7 @@ export class Terminal {
reaction(() => ThemeStore.getInstance().activeTheme.colors, this.setTheme, {
fireImmediately: true,
}),
dockStore.onResize(this.onResize),
dependencies.dockStore.onResize(this.onResize),
() => onDataHandler.dispose(),
() => this.fitAddon.dispose(),
() => this.api.removeAllListeners(),
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { UpgradeChartStore } from "./upgrade-chart.store";
import releaseStoreInjectable from "../../+apps-releases/release-store.injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable";
import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
const upgradeChartStoreInjectable = getInjectable({
instantiate: (di) => {
const createDockTabStore = di.inject(createDockTabStoreInjectable);
const valuesStore = createDockTabStore<string>();
return new UpgradeChartStore({
releaseStore: di.inject(releaseStoreInjectable),
dockStore: di.inject(dockStoreInjectable),
createStorage: di.inject(createStorageInjectable),
valuesStore,
});
},
lifecycle: lifecycleEnum.singleton,
});
export default upgradeChartStoreInjectable;
@@ -20,35 +20,44 @@
*/
import { action, autorun, computed, IReactionDisposer, reaction, makeObservable } from "mobx";
import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store";
import { DockTabStore } from "./dock-tab.store";
import { getReleaseValues, HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
import { releaseStore } from "../+apps-releases/release.store";
import { iter } from "../../utils";
import { DockStore, DockTab, TabId, TabKind } from "../dock-store/dock.store";
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
import { getReleaseValues } from "../../../../common/k8s-api/endpoints/helm-releases.api";
import type { ReleaseStore } from "../../+apps-releases/release.store";
import { iter, StorageHelper } from "../../../utils";
export interface IChartUpgradeData {
releaseName: string;
releaseNamespace: string;
}
interface Dependencies {
releaseStore: ReleaseStore
valuesStore: DockTabStore<string>
dockStore: DockStore
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
}
export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
private watchers = new Map<string, IReactionDisposer>();
values = new DockTabStore<string>();
@computed private get releaseNameReverseLookup(): Map<string, string> {
return new Map(iter.map(this.data, ([id, { releaseName }]) => [releaseName, id]));
}
constructor() {
super({
get values() {
return this.dependencies.valuesStore;
}
constructor(protected dependencies : Dependencies) {
super(dependencies, {
storageKey: "chart_releases",
});
makeObservable(this);
autorun(() => {
const { selectedTab, isOpen } = dockStore;
const { selectedTab, isOpen } = dependencies.dockStore;
if (selectedTab?.kind === TabKind.UPGRADE_CHART && isOpen) {
this.loadData(selectedTab.id);
@@ -67,20 +76,20 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
return;
}
const dispose = reaction(() => {
const release = releaseStore.getByName(releaseName);
const release = this.dependencies.releaseStore.getByName(releaseName);
return release?.getRevision(); // watch changes only by revision
},
release => {
const releaseTab = this.getTabByRelease(releaseName);
if (!releaseStore.isLoaded || !releaseTab) {
if (!this.dependencies.releaseStore.isLoaded || !releaseTab) {
return;
}
// auto-reload values if was loaded before
if (release) {
if (dockStore.selectedTab === releaseTab && this.values.getData(releaseTab.id)) {
if (this.dependencies.dockStore.selectedTab === releaseTab && this.values.getData(releaseTab.id)) {
this.loadValues(releaseTab.id);
}
}
@@ -88,17 +97,17 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
else {
dispose();
this.watchers.delete(releaseName);
dockStore.closeTab(releaseTab.id);
this.dependencies.dockStore.closeTab(releaseTab.id);
}
});
this.watchers.set(releaseName, dispose);
}
isLoading(tabId = dockStore.selectedTabId) {
isLoading(tabId = this.dependencies.dockStore.selectedTabId) {
const values = this.values.getData(tabId);
return !releaseStore.isLoaded || values === undefined;
return !this.dependencies.releaseStore.isLoaded || values === undefined;
}
@action
@@ -106,7 +115,7 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
const values = this.values.getData(tabId);
await Promise.all([
!releaseStore.isLoaded && releaseStore.loadFromContextNamespaces(),
!this.dependencies.releaseStore.isLoaded && this.dependencies.releaseStore.loadFromContextNamespaces(),
!values && this.loadValues(tabId),
]);
}
@@ -121,32 +130,6 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
}
getTabByRelease(releaseName: string): DockTab {
return dockStore.getTabById(this.releaseNameReverseLookup.get(releaseName));
return this.dependencies.dockStore.getTabById(this.releaseNameReverseLookup.get(releaseName));
}
}
export const upgradeChartStore = new UpgradeChartStore();
export function createUpgradeChartTab(release: HelmRelease, tabParams: DockTabCreateSpecific = {}) {
let tab = upgradeChartStore.getTabByRelease(release.getName());
if (tab) {
dockStore.open();
dockStore.selectTab(tab.id);
}
if (!tab) {
tab = dockStore.createTab({
title: `Helm Upgrade: ${release.getName()}`,
...tabParams,
kind: TabKind.UPGRADE_CHART,
}, false);
upgradeChartStore.setData(tab.id, {
releaseName: release.getName(),
releaseNamespace: release.getNs(),
});
}
return tab;
}
+31 -11
View File
@@ -25,29 +25,37 @@ import React from "react";
import { action, makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { cssNames } from "../../utils";
import type { DockTab } from "./dock.store";
import type { DockTab } from "./dock-store/dock.store";
import { InfoPanel } from "./info-panel";
import { upgradeChartStore } from "./upgrade-chart.store";
import type { UpgradeChartStore } from "./upgrade-chart-store/upgrade-chart.store";
import { Spinner } from "../spinner";
import { releaseStore } from "../+apps-releases/release.store";
import type { ReleaseStore } from "../+apps-releases/release.store";
import { Badge } from "../badge";
import { EditorPanel } from "./editor-panel";
import { helmChartStore, IChartVersion } from "../+apps-helm-charts/helm-chart.store";
import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
import { Select, SelectOption } from "../select";
import { withInjectables } from "@ogre-tools/injectable-react";
import releaseStoreInjectable from "../+apps-releases/release-store.injectable";
import upgradeChartStoreInjectable from "./upgrade-chart-store/upgrade-chart-store.injectable";
interface Props {
className?: string;
tab: DockTab;
}
interface Dependencies {
releaseStore: ReleaseStore
upgradeChartStore: UpgradeChartStore
}
@observer
export class UpgradeChart extends React.Component<Props> {
export class NonInjectedUpgradeChart extends React.Component<Props & Dependencies> {
@observable error: string;
@observable versions = observable.array<IChartVersion>();
@observable version: IChartVersion;
constructor(props: Props) {
constructor(props: Props & Dependencies) {
super(props);
makeObservable(this);
}
@@ -65,15 +73,15 @@ export class UpgradeChart extends React.Component<Props> {
}
get release(): HelmRelease {
const tabData = upgradeChartStore.getData(this.tabId);
const tabData = this.props.upgradeChartStore.getData(this.tabId);
if (!tabData) return null;
return releaseStore.getByName(tabData.releaseName);
return this.props.releaseStore.getByName(tabData.releaseName);
}
get value() {
return upgradeChartStore.values.getData(this.tabId);
return this.props.upgradeChartStore.values.getData(this.tabId);
}
async loadVersions() {
@@ -89,7 +97,7 @@ export class UpgradeChart extends React.Component<Props> {
@action
onChange = (value: string) => {
this.error = "";
upgradeChartStore.values.setData(this.tabId, value);
this.props.upgradeChartStore.values.setData(this.tabId, value);
};
@action
@@ -103,7 +111,7 @@ export class UpgradeChart extends React.Component<Props> {
const releaseName = this.release.getName();
const releaseNs = this.release.getNs();
await releaseStore.update(releaseName, releaseNs, {
await this.props.releaseStore.update(releaseName, releaseNs, {
chart: this.release.getChart(),
values: this.value,
repo, version,
@@ -127,7 +135,7 @@ export class UpgradeChart extends React.Component<Props> {
const { tabId, release, value, error, onChange, onError, upgrade, versions, version } = this;
const { className } = this.props;
if (!release || upgradeChartStore.isLoading() || !version) {
if (!release || this.props.upgradeChartStore.isLoading() || !version) {
return <Spinner center/>;
}
const currentVersion = release.getVersion();
@@ -169,3 +177,15 @@ export class UpgradeChart extends React.Component<Props> {
);
}
}
export const UpgradeChart = withInjectables<Dependencies, Props>(
NonInjectedUpgradeChart,
{
getProps: (di, props) => ({
releaseStore: di.inject(releaseStoreInjectable),
upgradeChartStore: di.inject(upgradeChartStoreInjectable),
...props,
}),
},
);