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

Fix endless re-rending of logs

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2022-01-10 12:14:10 +02:00
parent 4441d714dd
commit 48948c7286
No known key found for this signature in database
GPG Key ID: 5F465B5672372402
23 changed files with 354 additions and 161 deletions

View File

@ -27,11 +27,11 @@ import type { KubeJsonApiData } from "../kube-json-api";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
export class PodsApi extends KubeApi<Pod> { export class PodsApi extends KubeApi<Pod> {
async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> { getLogs = async (params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise<string> => {
const path = `${this.getUrl(params)}/log`; const path = `${this.getUrl(params)}/log`;
return this.request.get(path, { query }); return this.request.get(path, { query });
} };
} }
export function getMetricsForPods(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<IPodMetrics> { export function getMetricsForPods(pods: Pod[], namespace: string, selector = "pod, namespace"): Promise<IPodMetrics> {

View File

@ -58,7 +58,6 @@ const getComponent = (tabData: LogTabData) => {
tabId="tabId" tabId="tabId"
tabData={tabData} tabData={tabData}
save={jest.fn()} save={jest.fn()}
reload={jest.fn()}
/> />
); );
}; };

View File

@ -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 createStorageInjectable from "../../../../utils/create-storage/create-storage.injectable";
import { DockStorageState, TabKind } from "../dock.store";
const dockStorageInjectable = getInjectable({
instantiate: (di) => {
const createStorage = di.inject(createStorageInjectable);
return createStorage<DockStorageState>("dock", {
height: 300,
tabs: [
{
id: "terminal",
kind: TabKind.TERMINAL,
title: "Terminal",
pinned: false,
},
],
});
},
lifecycle: lifecycleEnum.singleton,
});
export default dockStorageInjectable;

View File

@ -19,29 +19,14 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { DockStorageState, DockStore, TabKind } from "./dock.store"; import { DockStore } from "./dock.store";
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; import dockStorageInjectable from "./dock-storage/dock-storage.injectable";
const dockStoreInjectable = getInjectable({ const dockStoreInjectable = getInjectable({
instantiate: (di) => { instantiate: (di) =>
const createStorage = di.inject(createStorageInjectable); new DockStore({
storage: di.inject(dockStorageInjectable),
const storage = createStorage<DockStorageState>("dock", { }),
height: 300,
tabs: [
{
id: "terminal",
kind: TabKind.TERMINAL,
title: "Terminal",
pinned: false,
},
],
});
return new DockStore({
storage,
});
},
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });

View File

@ -131,16 +131,18 @@ export class DockStore implements DockStorageState {
return this.dependencies.storage.whenReady; return this.dependencies.storage.whenReady;
} }
@computed
get isOpen(): boolean { get isOpen(): boolean {
return this.dependencies.storage.get().isOpen; return this.dependencies.storage.value.isOpen;
} }
set isOpen(isOpen: boolean) { set isOpen(isOpen: boolean) {
this.dependencies.storage.merge({ isOpen }); this.dependencies.storage.merge({ isOpen });
} }
@computed
get height(): number { get height(): number {
return this.dependencies.storage.get().height; return this.dependencies.storage.value.height;
} }
set height(height: number) { set height(height: number) {
@ -149,20 +151,22 @@ export class DockStore implements DockStorageState {
}); });
} }
@computed
get tabs(): DockTab[] { get tabs(): DockTab[] {
return this.dependencies.storage.get().tabs; return this.dependencies.storage.value.tabs;
} }
set tabs(tabs: DockTab[]) { set tabs(tabs: DockTab[]) {
this.dependencies.storage.merge({ tabs }); this.dependencies.storage.merge({ tabs });
} }
@computed
get selectedTabId(): TabId | undefined { get selectedTabId(): TabId | undefined {
return this.dependencies.storage.get().selectedTabId const storageData = this.dependencies.storage.value;
|| (
this.tabs.length > 0 return (
? this.tabs[0]?.id storageData.selectedTabId ||
: undefined (this.tabs.length > 0 ? this.tabs[0]?.id : undefined)
); );
} }

View File

@ -58,8 +58,9 @@ export class DockTabStore<T> {
// auto-save to local-storage // auto-save to local-storage
if (storageKey) { if (storageKey) {
this.storage = this.dependencies.createStorage<T>(storageKey, {}); this.storage = this.dependencies.createStorage<T>(storageKey, {});
this.storage.whenReady.then(() => { this.storage.whenReady.then(() => {
this.data.replace(this.storage.get()); this.data.replace(this.storage.value);
reaction(() => this.toJSON(), data => this.storage.set(data)); reaction(() => this.toJSON(), data => this.storage.set(data));
}); });
} }

View File

@ -101,7 +101,7 @@ class NonInjectedDock extends React.Component<Props & Dependencies> {
case TabKind.UPGRADE_CHART: case TabKind.UPGRADE_CHART:
return <UpgradeChart tab={tab} />; return <UpgradeChart tab={tab} />;
case TabKind.POD_LOGS: case TabKind.POD_LOGS:
return <Logs tab={tab} />; return <Logs />;
case TabKind.TERMINAL: case TabKind.TERMINAL:
return <TerminalWindow tab={tab} />; return <TerminalWindow tab={tab} />;
} }

View File

@ -37,7 +37,6 @@ interface Props {
tabData?: LogTabData tabData?: LogTabData
logs: string[] logs: string[]
save: (data: Partial<LogTabData>) => void save: (data: Partial<LogTabData>) => void
reload: () => void
} }
interface Dependencies { interface Dependencies {
@ -45,7 +44,7 @@ interface Dependencies {
} }
const NonInjectedLogControls = observer((props: Props & Dependencies) => { const NonInjectedLogControls = observer((props: Props & Dependencies) => {
const { tabData, save, reload, logs, logStore } = props; const { tabData, save, logs, logStore } = props;
if (!tabData) { if (!tabData) {
return null; return null;
@ -61,7 +60,7 @@ const NonInjectedLogControls = observer((props: Props & Dependencies) => {
const togglePrevious = () => { const togglePrevious = () => {
save({ previous: !previous }); save({ previous: !previous });
reload(); logStore.reload();
}; };
const downloadLogs = () => { const downloadLogs = () => {

View File

@ -32,7 +32,6 @@ import type { Align, ListOnScrollProps } from "react-window";
import { SearchStore } from "../../search-store/search-store"; import { SearchStore } from "../../search-store/search-store";
import { UserStore } from "../../../common/user-store"; import { UserStore } from "../../../common/user-store";
import { array, boundMethod, cssNames } from "../../utils"; import { array, boundMethod, cssNames } from "../../utils";
import { Spinner } from "../spinner";
import { VirtualList } from "../virtual-list"; import { VirtualList } from "../virtual-list";
import type { LogStore } from "./log-store/log.store"; import type { LogStore } from "./log-store/log.store";
import type { LogTabStore } from "./log-tab-store/log-tab.store"; import type { LogTabStore } from "./log-tab-store/log-tab.store";
@ -44,8 +43,6 @@ import searchStoreInjectable from "../../search-store/search-store.injectable";
interface Props { interface Props {
logs: string[] logs: string[]
isLoading: boolean
load: () => void
id: string id: string
} }
@ -58,7 +55,7 @@ interface Dependencies {
} }
@observer @observer
class NonInjectedLogList extends React.Component<Props & Dependencies> { export class NonInjectedLogList extends React.Component<Props & Dependencies> {
@observable isJumpButtonVisible = false; @observable isJumpButtonVisible = false;
@observable isLastLineVisible = true; @observable isLastLineVisible = true;
@ -165,7 +162,7 @@ class NonInjectedLogList extends React.Component<Props & Dependencies> {
const { scrollOffset } = props; const { scrollOffset } = props;
if (scrollOffset === 0) { if (scrollOffset === 0) {
this.props.load(); this.props.logStore.load();
} }
}; };
@ -241,18 +238,8 @@ class NonInjectedLogList extends React.Component<Props & Dependencies> {
}; };
render() { render() {
const { isLoading } = this.props;
const isInitLoading = isLoading && !this.logs.length;
const rowHeights = array.filled(this.logs.length, this.lineHeight); const rowHeights = array.filled(this.logs.length, this.lineHeight);
if (isInitLoading) {
return (
<div className="LogList flex box grow align-center justify-center">
<Spinner center/>
</div>
);
}
if (!this.logs.length) { if (!this.logs.length) {
return ( return (
<div className="LogList flex box grow align-center justify-center"> <div className="LogList flex box grow align-center justify-center">
@ -262,7 +249,7 @@ class NonInjectedLogList extends React.Component<Props & Dependencies> {
} }
return ( return (
<div className={cssNames("LogList flex", { isLoading })}> <div className={cssNames("LogList flex" )}>
<VirtualList <VirtualList
items={this.logs} items={this.logs}
rowHeights={rowHeights} rowHeights={rowHeights}

View File

@ -32,20 +32,21 @@ import { podsStore } from "../+workloads-pods/pods.store";
import type { TabId } from "./dock-store/dock.store"; import type { TabId } from "./dock-store/dock.store";
import logTabStoreInjectable from "./log-tab-store/log-tab-store.injectable"; import logTabStoreInjectable from "./log-tab-store/log-tab-store.injectable";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import logStoreInjectable from "./log-store/log-store.injectable";
interface Props { interface Props {
tabId: TabId tabId: TabId
tabData: LogTabData tabData: LogTabData
save: (data: Partial<LogTabData>) => void save: (data: Partial<LogTabData>) => void
reload: () => void
} }
interface Dependencies { interface Dependencies {
logTabStore: LogTabStore logTabStore: LogTabStore
reloadLogs: () => Promise<void>
} }
const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) => { const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) => {
const { tabData, save, reload, tabId, logTabStore } = props; const { tabData, save, tabId, logTabStore, reloadLogs } = props;
const { selectedPod, selectedContainer, pods } = tabData; const { selectedPod, selectedContainer, pods } = tabData;
const pod = new Pod(selectedPod); const pod = new Pod(selectedPod);
const containers = pod.getContainers(); const containers = pod.getContainers();
@ -57,7 +58,8 @@ const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) =>
.concat(initContainers) .concat(initContainers)
.find(container => container.name === option.value), .find(container => container.name === option.value),
}); });
reload();
reloadLogs();
}; };
const onPodChange = (option: SelectOption) => { const onPodChange = (option: SelectOption) => {
@ -96,7 +98,7 @@ const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) =>
]; ];
useEffect(() => { useEffect(() => {
reload(); reloadLogs();
}, [selectedPod]); }, [selectedPod]);
return ( return (
@ -128,6 +130,7 @@ export const LogResourceSelector = withInjectables<Dependencies, Props>(
{ {
getProps: (di, props) => ({ getProps: (di, props) => ({
logTabStore: di.inject(logTabStoreInjectable), logTabStore: di.inject(logTabStoreInjectable),
reloadLogs: di.inject(logStoreInjectable).reload,
...props, ...props,
}), }),
}, },

View File

@ -0,0 +1,29 @@
/**
* 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 { podsApi } from "../../../../../common/k8s-api/endpoints";
const callForLogsInjectable = getInjectable({
instantiate: () => podsApi.getLogs,
lifecycle: lifecycleEnum.singleton,
});
export default callForLogsInjectable;

View File

@ -22,11 +22,13 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { LogStore } from "./log.store"; import { LogStore } from "./log.store";
import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable"; import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable";
import dockStoreInjectable from "../dock-store/dock-store.injectable"; import dockStoreInjectable from "../dock-store/dock-store.injectable";
import callForLogsInjectable from "./call-for-logs/call-for-logs.injectable";
const logStoreInjectable = getInjectable({ const logStoreInjectable = getInjectable({
instantiate: (di) => new LogStore({ instantiate: (di) => new LogStore({
logTabStore: di.inject(logTabStoreInjectable), logTabStore: di.inject(logTabStoreInjectable),
dockStore: di.inject(dockStoreInjectable), dockStore: di.inject(dockStoreInjectable),
callForLogs: di.inject(callForLogsInjectable),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,

View File

@ -21,7 +21,7 @@
import { autorun, computed, observable, makeObservable } from "mobx"; import { autorun, computed, observable, makeObservable } from "mobx";
import { IPodLogsQuery, Pod, podsApi } from "../../../../common/k8s-api/endpoints"; import { IPodLogsQuery, Pod } from "../../../../common/k8s-api/endpoints";
import { autoBind, interval } from "../../../utils"; import { autoBind, interval } from "../../../utils";
import { DockStore, TabId, TabKind } from "../dock-store/dock.store"; import { DockStore, TabId, TabKind } from "../dock-store/dock.store";
import type { LogTabStore } from "../log-tab-store/log-tab.store"; import type { LogTabStore } from "../log-tab-store/log-tab.store";
@ -33,6 +33,7 @@ const logLinesToLoad = 500;
interface Dependencies { interface Dependencies {
logTabStore: LogTabStore logTabStore: LogTabStore
dockStore: DockStore dockStore: DockStore
callForLogs: ({ namespace, name }: { namespace: string, name: string }, query: IPodLogsQuery) => Promise<string>
} }
export class LogStore { export class LogStore {
@ -79,9 +80,10 @@ export class LogStore {
* Each time it increasing it's number, caused to fetch more logs. * Each time it increasing it's number, caused to fetch more logs.
* Also, it handles loading errors, rewriting whole logs with error * Also, it handles loading errors, rewriting whole logs with error
* messages * messages
* @param tabId
*/ */
load = async (tabId: TabId) => { load = async () => {
const tabId = this.dependencies.dockStore.selectedTabId;
try { try {
const logs = await this.loadLogs(tabId, { const logs = await this.loadLogs(tabId, {
tailLines: this.lines + logLinesToLoad, tailLines: this.lines + logLinesToLoad,
@ -127,12 +129,13 @@ export class LogStore {
*/ */
async loadLogs(tabId: TabId, params: Partial<IPodLogsQuery>): Promise<string[]> { async loadLogs(tabId: TabId, params: Partial<IPodLogsQuery>): Promise<string[]> {
const data = this.dependencies.logTabStore.getData(tabId); const data = this.dependencies.logTabStore.getData(tabId);
const { selectedContainer, previous } = data; const { selectedContainer, previous } = data;
const pod = new Pod(data.selectedPod); const pod = new Pod(data.selectedPod);
const namespace = pod.getNs(); const namespace = pod.getNs();
const name = pod.getName(); const name = pod.getName();
const result = await podsApi.getLogs({ namespace, name }, { const result = await this.dependencies.callForLogs({ namespace, name }, {
...params, ...params,
timestamps: true, // Always setting timestamp to separate old logs from new ones timestamps: true, // Always setting timestamp to separate old logs from new ones
container: selectedContainer.name, container: selectedContainer.name,
@ -205,4 +208,10 @@ export class LogStore {
clearLogs(tabId: TabId) { clearLogs(tabId: TabId) {
this.podLogs.delete(tabId); this.podLogs.delete(tabId);
} }
reload = async () => {
this.clearLogs(this.dependencies.dockStore.selectedTabId);
await this.load();
};
} }

View File

@ -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 logStoreInjectable from "./log-store.injectable";
const reloadedLogStoreInjectable = getInjectable({
instantiate: async (di) => {
const nonReloadedStore = di.inject(logStoreInjectable);
await nonReloadedStore.reload();
return nonReloadedStore;
},
lifecycle: lifecycleEnum.transient,
});
export default reloadedLogStoreInjectable;

View File

@ -20,7 +20,7 @@
*/ */
import uniqueId from "lodash/uniqueId"; import uniqueId from "lodash/uniqueId";
import { reaction } from "mobx"; import { computed, makeObservable, 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 { IPodContainer, Pod } from "../../../../common/k8s-api/endpoints";
@ -59,6 +59,14 @@ export class LogTabStore extends DockTabStore<LogTabData> {
}); });
reaction(() => podsStore.items.length, () => this.updateTabsData()); reaction(() => podsStore.items.length, () => this.updateTabsData());
makeObservable(this, {
tabs: computed,
});
}
get tabs() {
return this.data.get(this.dependencies.dockStore.selectedTabId);
} }
createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): string { createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): string {

View File

@ -20,87 +20,46 @@
*/ */
import React from "react"; import React from "react";
import { observable, reaction, makeObservable } from "mobx"; import { observer } from "mobx-react";
import { disposeOnUnmount, observer } from "mobx-react";
import { boundMethod } from "../../utils"; import { boundMethod } from "../../utils";
import type { DockTab } from "./dock-store/dock.store";
import { InfoPanel } from "./info-panel"; import { InfoPanel } from "./info-panel";
import { LogResourceSelector } from "./log-resource-selector"; import { LogResourceSelector } from "./log-resource-selector";
import { LogList } from "./log-list"; import { LogList, NonInjectedLogList } from "./log-list";
import type { LogStore } from "./log-store/log.store";
import { LogSearch } from "./log-search"; import { LogSearch } from "./log-search";
import { LogControls } from "./log-controls"; import { LogControls } from "./log-controls";
import type { LogTabData, LogTabStore } from "./log-tab-store/log-tab.store";
import { withInjectables } from "@ogre-tools/injectable-react"; 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 type { SearchStore } from "../../search-store/search-store";
import searchStoreInjectable from "../../search-store/search-store.injectable"; import searchStoreInjectable from "../../search-store/search-store.injectable";
import { Spinner } from "../spinner";
import logsViewModelInjectable from "./logs/logs-view-model/logs-view-model.injectable";
import type { LogsViewModel } from "./logs/logs-view-model/logs-view-model";
interface Props { interface Props {
className?: string className?: string;
tab: DockTab
} }
interface Dependencies { interface Dependencies {
logTabStore: LogTabStore
logStore: LogStore
searchStore: SearchStore searchStore: SearchStore
model: LogsViewModel
} }
@observer @observer
class NonInjectedLogs extends React.Component<Props & Dependencies> { class NonInjectedLogs extends React.Component<Props & Dependencies> {
@observable isLoading = true; private logListElement = React.createRef<NonInjectedLogList>(); // A reference for VirtualList component
private logListElement = React.createRef<typeof LogList>(); // A reference for VirtualList component get model() {
return this.props.model;
constructor(props: Props & Dependencies) {
super(props);
makeObservable(this);
}
componentDidMount() {
disposeOnUnmount(this,
reaction(() => this.props.tab.id, this.reload, { fireImmediately: true }),
);
}
get tabId() {
return this.props.tab.id;
}
load = async () => {
this.isLoading = true;
await this.props.logStore.load(this.tabId);
this.isLoading = false;
};
reload = async () => {
this.props.logStore.clearLogs(this.tabId);
await this.load();
};
/**
* A function for various actions after search is happened
* @param query {string} A text from search field
*/
@boundMethod
onSearch() {
this.toOverlay();
} }
/** /**
* Scrolling to active overlay (search word highlight) * Scrolling to active overlay (search word highlight)
*/ */
@boundMethod @boundMethod
toOverlay() { scrollToOverlay() {
const { activeOverlayLine } = this.props.searchStore; const { activeOverlayLine } = this.props.searchStore;
if (!this.logListElement.current || activeOverlayLine === undefined) return; if (!this.logListElement.current || activeOverlayLine === undefined) return;
// Scroll vertically // Scroll vertically
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.logListElement.current.scrollToItem(activeOverlayLine, "center"); this.logListElement.current.scrollToItem(activeOverlayLine, "center");
// Scroll horizontally in timeout since virtual list need some time to prepare its contents // Scroll horizontally in timeout since virtual list need some time to prepare its contents
setTimeout(() => { setTimeout(() => {
@ -111,33 +70,35 @@ class NonInjectedLogs extends React.Component<Props & Dependencies> {
}, 100); }, 100);
} }
renderResourceSelector(data?: LogTabData) { renderResourceSelector() {
if (!data) { const { tabs, logs, logsWithoutTimestamps, saveTab, tabId } = this.model;
if (!tabs) {
return null; return null;
} }
const logs = this.props.logStore.logs; const searchLogs = tabs.showTimestamps ? logs : logsWithoutTimestamps;
const searchLogs = data.showTimestamps ? logs : this.props.logStore.logsWithoutTimestamps;
const controls = ( const controls = (
<div className="flex gaps"> <div className="flex gaps">
<LogResourceSelector <LogResourceSelector
tabId={this.tabId} tabId={tabId}
tabData={data} tabData={tabs}
save={newData => this.props.logTabStore.setData(this.tabId, { ...data, ...newData })} save={saveTab}
reload={this.reload}
/> />
<LogSearch <LogSearch
onSearch={this.onSearch} onSearch={this.scrollToOverlay}
logs={searchLogs} logs={searchLogs}
toPrevOverlay={this.toOverlay} toPrevOverlay={this.scrollToOverlay}
toNextOverlay={this.toOverlay} toNextOverlay={this.scrollToOverlay}
/> />
</div> </div>
); );
return ( return (
<InfoPanel <InfoPanel
tabId={this.props.tab.id} tabId={this.model.tabId}
controls={controls} controls={controls}
showSubmitClose={false} showSubmitClose={false}
showButtons={false} showButtons={false}
@ -147,44 +108,44 @@ class NonInjectedLogs extends React.Component<Props & Dependencies> {
} }
render() { render() {
const logs = this.props.logStore.logs; const { logs, tabs, tabId, saveTab } = this.model;
const data = this.props.logTabStore.getData(this.tabId);
if (!data) {
this.reload();
}
return ( return (
<div className="PodLogs flex column"> <div className="PodLogs flex column">
{this.renderResourceSelector(data)} {this.renderResourceSelector()}
<LogList <LogList
logs={logs} logs={logs}
id={this.tabId} id={tabId}
isLoading={this.isLoading}
load={this.load}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
ref={this.logListElement} ref={this.logListElement}
/> />
<LogControls <LogControls
logs={logs} logs={logs}
tabData={data} tabData={tabs}
save={newData => this.props.logTabStore.setData(this.tabId, { ...data, ...newData })} save={saveTab}
reload={this.reload}
/> />
</div> </div>
); );
} }
} }
export const Logs = withInjectables<Dependencies, Props>( export const Logs = withInjectables<Dependencies, Props>(
NonInjectedLogs, NonInjectedLogs,
{ {
getProps: (di, props) => ({
logTabStore: di.inject(logTabStoreInjectable), getPlaceholder: () => (
logStore: di.inject(logStoreInjectable), <div className="flex box grow align-center justify-center">
<Spinner center />
</div>
),
getProps: async (di, props) => ({
searchStore: di.inject(searchStoreInjectable), searchStore: di.inject(searchStoreInjectable),
model: await di.inject(logsViewModelInjectable),
...props, ...props,
}), }),
}, },

View File

@ -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 dockStoreInjectable from "../../dock-store/dock-store.injectable";
import logTabStoreInjectable from "../../log-tab-store/log-tab-store.injectable";
import reloadedLogStoreInjectable from "../../log-store/reloaded-log-store.injectable";
import { LogsViewModel } from "./logs-view-model";
const logsViewModelInjectable = getInjectable({
instantiate: async (di) => new LogsViewModel({
dockStore: di.inject(dockStoreInjectable),
logTabStore: di.inject(logTabStoreInjectable),
logStore: await di.inject(reloadedLogStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default logsViewModelInjectable;

View File

@ -0,0 +1,61 @@
/**
* 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 { LogTabData, LogTabStore } from "../../log-tab-store/log-tab.store";
import type { LogStore } from "../../log-store/log.store";
import { computed } from "mobx";
import { makeObservable } from "mobx";
interface Dependencies {
dockStore: { selectedTabId: string },
logTabStore: LogTabStore
logStore: LogStore
}
export class LogsViewModel {
constructor(private dependencies: Dependencies) {
makeObservable(this, {
logs: computed,
logsWithoutTimestamps: computed,
tabs: computed,
tabId: computed,
});
}
get logs() {
return this.dependencies.logStore.logs;
}
get logsWithoutTimestamps() {
return this.dependencies.logStore.logsWithoutTimestamps;
}
get tabs() {
return this.dependencies.logTabStore.tabs;
}
get tabId() {
return this.dependencies.dockStore.selectedTabId;
}
saveTab = (newTabs: LogTabData) => {
this.dependencies.logTabStore.setData(this.tabId, { ...this.tabs, ...newTabs });
};
}

View File

@ -546,7 +546,10 @@ class NonInjectedItemListLayout<I extends ItemObject> extends React.Component<It
export function ItemListLayout<I extends ItemObject>( export function ItemListLayout<I extends ItemObject>(
props: ItemListLayoutProps<I>, props: ItemListLayoutProps<I>,
) { ) {
return withInjectables<Dependencies, ItemListLayoutProps<I>>( const InjectedItemListLayout = withInjectables<
Dependencies,
ItemListLayoutProps<I>
>(
NonInjectedItemListLayout, NonInjectedItemListLayout,
{ {
@ -556,5 +559,7 @@ export function ItemListLayout<I extends ItemObject>(
...props, ...props,
}), }),
}, },
)(props); );
return <InjectedItemListLayout {...props} />;
} }

View File

@ -157,7 +157,10 @@ class NonInjectedKubeObjectListLayout<K extends KubeObject> extends React.Compon
export function KubeObjectListLayout<K extends KubeObject>( export function KubeObjectListLayout<K extends KubeObject>(
props: KubeObjectListLayoutProps<K>, props: KubeObjectListLayoutProps<K>,
) { ) {
return withInjectables<Dependencies, KubeObjectListLayoutProps<K>>( const InjectedKubeObjectListLayout = withInjectables<
Dependencies,
KubeObjectListLayoutProps<K>
>(
NonInjectedKubeObjectListLayout, NonInjectedKubeObjectListLayout,
{ {
@ -167,5 +170,7 @@ export function KubeObjectListLayout<K extends KubeObject>(
...props, ...props,
}), }),
}, },
)(props); );
return <InjectedKubeObjectListLayout {...props} />;
} }

View File

@ -127,7 +127,7 @@ class NonInjectedKubeObjectMenu<TKubeObject extends KubeObject> extends React.Co
export function KubeObjectMenu<T extends KubeObject>( export function KubeObjectMenu<T extends KubeObject>(
props: KubeObjectMenuProps<T>, props: KubeObjectMenuProps<T>,
) { ) {
return withInjectables<Dependencies, KubeObjectMenuProps<T>>( const InjectedKubeObjectMenu = withInjectables<Dependencies, KubeObjectMenuProps<T>>(
NonInjectedKubeObjectMenu, NonInjectedKubeObjectMenu,
{ {
getProps: (di, props) => ({ getProps: (di, props) => ({
@ -142,5 +142,7 @@ export function KubeObjectMenu<T extends KubeObject>(
...props, ...props,
}), }),
}, },
)(props); );
return <InjectedKubeObjectMenu {...props} />;
} }

View File

@ -263,7 +263,7 @@ class NonInjectedTable<Item> extends React.Component<TableProps<Item> & Dependen
} }
export function Table<Item>(props: TableProps<Item>) { export function Table<Item>(props: TableProps<Item>) {
return withInjectables<Dependencies, TableProps<Item>>( const InjectedTable = withInjectables<Dependencies, TableProps<Item>>(
NonInjectedTable, NonInjectedTable,
{ {
@ -272,6 +272,8 @@ export function Table<Item>(props: TableProps<Item>) {
...props, ...props,
}), }),
}, },
)(props); );
return <InjectedTable {...props} />;
} }

View File

@ -20,7 +20,15 @@
*/ */
// Helper for working with storages (e.g. window.localStorage, NodeJS/file-system, etc.) // Helper for working with storages (e.g. window.localStorage, NodeJS/file-system, etc.)
import { action, comparer, makeObservable, observable, toJS, when } from "mobx"; import {
action,
comparer,
computed,
makeObservable,
observable,
toJS,
when,
} from "mobx";
import produce, { Draft, isDraft } from "immer"; import produce, { Draft, isDraft } from "immer";
import { isEqual, isPlainObject } from "lodash"; import { isEqual, isPlainObject } from "lodash";
import logger from "../../main/logger"; import logger from "../../main/logger";
@ -66,10 +74,6 @@ export class StorageHelper<T> {
this.storage = storage; this.storage = storage;
this.data.observe_(({ newValue, oldValue }) => {
this.onChange(newValue as T, oldValue as T);
});
if (autoInit) { if (autoInit) {
this.init(); this.init();
} }
@ -130,16 +134,25 @@ export class StorageHelper<T> {
} }
get(): T { get(): T {
return this.value;
}
@computed
get value(): T {
return this.data.get() ?? this.defaultValue; return this.data.get() ?? this.defaultValue;
} }
@action @action
set(value: T) { set(newValue: T) {
if (this.isDefaultValue(value)) { const oldValue = this.value;
if (this.isDefaultValue(newValue)) {
this.reset(); this.reset();
} else { } else {
this.data.set(value); this.data.set(newValue);
} }
this.onChange(newValue, oldValue);
} }
@action @action