diff --git a/src/renderer/components/dock/logs/are-logs-present.injectable.ts b/src/renderer/components/dock/logs/are-logs-present.injectable.ts new file mode 100644 index 0000000000..7998e0ff2c --- /dev/null +++ b/src/renderer/components/dock/logs/are-logs-present.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { bind } from "../../../utils"; +import type { TabId } from "../dock/store"; +import type { LogStore } from "./store"; +import logStoreInjectable from "./store.injectable"; + +interface Dependencies { + logStore: LogStore; +} + +function areLogsPresent({ logStore }: Dependencies, tabId: TabId) { + return logStore.areLogsPresent(tabId); +} + +const areLogsPresentInjectable = getInjectable({ + instantiate: (di) => bind(areLogsPresent, null, { + logStore: di.inject(logStoreInjectable), + }), + lifecycle: lifecycleEnum.singleton, +}); + +export default areLogsPresentInjectable; diff --git a/src/renderer/components/dock/logs/list.tsx b/src/renderer/components/dock/logs/list.tsx index 42e8490a27..8361fa7f68 100644 --- a/src/renderer/components/dock/logs/list.tsx +++ b/src/renderer/components/dock/logs/list.tsx @@ -19,6 +19,7 @@ import { array, boundMethod, cssNames } from "../../../utils"; import { VirtualList } from "../../virtual-list"; import { ToBottom } from "./to-bottom"; import type { LogTabViewModel } from "../logs/logs-view-model"; +import { Spinner } from "../../spinner"; export interface LogListProps { model: LogTabViewModel; @@ -212,10 +213,18 @@ export class LogList extends React.Component { }; render() { + if (this.props.model.isLoading.get()) { + return ( +
+ +
+ ); + } + if (!this.logs.length) { return (
- There are no logs available for container + There are no logs available for container {this.props.model.logTabData.get()?.selectedContainer}
); } diff --git a/src/renderer/components/dock/logs/load-logs.injectable.ts b/src/renderer/components/dock/logs/load-logs.injectable.ts index 8b7f2e4eae..ea74b93505 100644 --- a/src/renderer/components/dock/logs/load-logs.injectable.ts +++ b/src/renderer/components/dock/logs/load-logs.injectable.ts @@ -4,6 +4,7 @@ */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import type { IComputedValue } from "mobx"; +import type { Pod } from "../../../../common/k8s-api/endpoints"; import { bind } from "../../../utils"; import type { LogStore } from "./store"; import logStoreInjectable from "./store.injectable"; @@ -13,8 +14,8 @@ interface Dependencies { logStore: LogStore; } -function loadLogs({ logStore }: Dependencies, tabId: string, logTabData: IComputedValue): Promise { - return logStore.load(tabId, logTabData); +function loadLogs({ logStore }: Dependencies, tabId: string, pod: IComputedValue, logTabData: IComputedValue): Promise { + return logStore.load(tabId, pod, logTabData); } const loadLogsInjectable = getInjectable({ diff --git a/src/renderer/components/dock/logs/logs-view-model.injectable.ts b/src/renderer/components/dock/logs/logs-view-model.injectable.ts index 9969191f4f..cc7012d7b1 100644 --- a/src/renderer/components/dock/logs/logs-view-model.injectable.ts +++ b/src/renderer/components/dock/logs/logs-view-model.injectable.ts @@ -16,6 +16,7 @@ import stopLoadingLogsInjectable from "./stop-loading-logs.injectable"; import { podsStore } from "../../+workloads-pods/pods.store"; import createSearchStoreInjectable from "../../../search-store/create-search-store.injectable"; import renameTabInjectable from "../dock/rename-tab.injectable"; +import areLogsPresentInjectable from "./are-logs-present.injectable"; export interface InstantiateArgs { tabId: TabId; @@ -32,6 +33,7 @@ const logsViewModelInjectable = getInjectable({ loadLogs: di.inject(loadLogsInjectable), renameTab: di.inject(renameTabInjectable), stopLoadingLogs: di.inject(stopLoadingLogsInjectable), + areLogsPresent: di.inject(areLogsPresentInjectable), getPodById: id => podsStore.getById(id), getPodsByOwnerId: id => podsStore.getPodsByOwnerId(id), createSearchStore: di.inject(createSearchStoreInjectable), diff --git a/src/renderer/components/dock/logs/logs-view-model.ts b/src/renderer/components/dock/logs/logs-view-model.ts index 5598298c53..fcc42ba113 100644 --- a/src/renderer/components/dock/logs/logs-view-model.ts +++ b/src/renderer/components/dock/logs/logs-view-model.ts @@ -14,18 +14,20 @@ export interface LogTabViewModelDependencies { getTimestampSplitLogs: (tabId: TabId) => [string, string][]; getLogTabData: (tabId: TabId) => LogTabData; setLogTabData: (tabId: TabId, data: LogTabData) => void; - loadLogs: (tabId: TabId, logTabData: IComputedValue) => Promise; - reloadLogs: (tabId: TabId, logTabData: IComputedValue) => Promise; + loadLogs: (tabId: TabId, pod: IComputedValue, logTabData: IComputedValue) => Promise; + reloadLogs: (tabId: TabId, pod: IComputedValue, logTabData: IComputedValue) => Promise; renameTab: (tabId: TabId, title: string) => void; stopLoadingLogs: (tabId: TabId) => void; getPodById: (id: string) => Pod | undefined; getPodsByOwnerId: (id: string) => Pod[]; + areLogsPresent: (tabId: TabId) => boolean; createSearchStore: () => SearchStore; } export class LogTabViewModel { constructor(protected readonly tabId: TabId, private readonly dependencies: LogTabViewModelDependencies) {} + readonly isLoading = computed(() => this.dependencies.areLogsPresent(this.tabId)); readonly logs = computed(() => this.dependencies.getLogs(this.tabId)); readonly logsWithoutTimestamps = computed(() => this.dependencies.getLogsWithoutTimestamps(this.tabId)); readonly timestampSplitLogs = computed(() => this.dependencies.getTimestampSplitLogs(this.tabId)); @@ -58,8 +60,8 @@ export class LogTabViewModel { this.dependencies.setLogTabData(this.tabId, { ...this.logTabData.get(), ...partialData }); }; - loadLogs = () => this.dependencies.loadLogs(this.tabId, this.logTabData); - reloadLogs = () => this.dependencies.reloadLogs(this.tabId, this.logTabData); + loadLogs = () => this.dependencies.loadLogs(this.tabId, this.pod, this.logTabData); + reloadLogs = () => this.dependencies.reloadLogs(this.tabId, this.pod, this.logTabData); renameTab = (title: string) => this.dependencies.renameTab(this.tabId, title); stopLoadingLogs = () => this.dependencies.stopLoadingLogs(this.tabId); } diff --git a/src/renderer/components/dock/logs/reload-logs.injectable.ts b/src/renderer/components/dock/logs/reload-logs.injectable.ts index 4ed8874e59..9081e34c91 100644 --- a/src/renderer/components/dock/logs/reload-logs.injectable.ts +++ b/src/renderer/components/dock/logs/reload-logs.injectable.ts @@ -4,6 +4,7 @@ */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import type { IComputedValue } from "mobx"; +import type { Pod } from "../../../../common/k8s-api/endpoints"; import { bind } from "../../../utils"; import type { LogStore } from "./store"; import logStoreInjectable from "./store.injectable"; @@ -13,8 +14,8 @@ interface Dependencies { logStore: LogStore; } -function reloadLogs({ logStore }: Dependencies, tabId: string, logTabData: IComputedValue): Promise { - return logStore.reload(tabId, logTabData); +function reloadLogs({ logStore }: Dependencies, tabId: string, pod: IComputedValue, logTabData: IComputedValue): Promise { + return logStore.reload(tabId, pod, logTabData); } const reloadLogsInjectable = getInjectable({ diff --git a/src/renderer/components/dock/logs/store.injectable.ts b/src/renderer/components/dock/logs/store.injectable.ts index cf92ab1fda..a1f4b79de9 100644 --- a/src/renderer/components/dock/logs/store.injectable.ts +++ b/src/renderer/components/dock/logs/store.injectable.ts @@ -5,12 +5,10 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { LogStore } from "./store"; import callForLogsInjectable from "./call-for-logs.injectable"; -import { podsStore } from "../../+workloads-pods/pods.store"; const logStoreInjectable = getInjectable({ instantiate: (di) => new LogStore({ callForLogs: di.inject(callForLogsInjectable), - getPodById: id => podsStore.getById(id), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/renderer/components/dock/logs/store.ts b/src/renderer/components/dock/logs/store.ts index 53206aff09..fb0c446119 100644 --- a/src/renderer/components/dock/logs/store.ts +++ b/src/renderer/components/dock/logs/store.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { observable, IComputedValue } from "mobx"; +import { observable, IComputedValue, when } from "mobx"; import type { IPodLogsQuery, Pod } from "../../../../common/k8s-api/endpoints"; import { getOrInsertWith, interval, IntervalFn } from "../../../utils"; import type { TabId } from "../dock/store"; @@ -14,7 +14,6 @@ type PodLogLine = string; const logLinesToLoad = 500; interface Dependencies { - getPodById: (id: string) => Pod | undefined; callForLogs: ({ namespace, name }: { namespace: string, name: string }, query: IPodLogsQuery) => Promise } @@ -44,24 +43,24 @@ export class LogStore { * Also, it handles loading errors, rewriting whole logs with error * messages */ - public async load(tabId: TabId, logTabData: IComputedValue): Promise { + public async load(tabId: TabId, computedPod: IComputedValue, logTabData: IComputedValue): Promise { try { - const logs = await this.loadLogs(logTabData, { + const logs = await this.loadLogs(computedPod, logTabData, { tailLines: this.getLogLines(tabId) + logLinesToLoad, }); - this.getRefresher(tabId, logTabData).start(); + this.getRefresher(tabId, computedPod, logTabData).start(); this.podLogs.set(tabId, logs); } catch (error) { this.handlerError(tabId, error); } } - private getRefresher(tabId: TabId, logTabData: IComputedValue): IntervalFn { + private getRefresher(tabId: TabId, computedPod: IComputedValue, logTabData: IComputedValue): IntervalFn { return getOrInsertWith(this.refreshers, tabId, () => ( interval(10, () => { if (this.podLogs.has(tabId)) { - this.loadMore(tabId, logTabData); + this.loadMore(tabId, computedPod, logTabData); } }) )); @@ -81,14 +80,14 @@ export class LogStore { * starting from last line received. * @param tabId */ - public async loadMore(tabId: TabId, logTabData: IComputedValue): Promise { + public async loadMore(tabId: TabId, computedPod: IComputedValue, logTabData: IComputedValue): Promise { if (!this.podLogs.get(tabId).length) { return; } try { const oldLogs = this.podLogs.get(tabId); - const logs = await this.loadLogs(logTabData, { + const logs = await this.loadLogs(computedPod, logTabData, { sinceTime: this.getLastSinceTime(tabId), }); @@ -106,9 +105,11 @@ export class LogStore { * @param params request parameters described in IPodLogsQuery interface * @returns A fetch request promise */ - private async loadLogs(logTabData: IComputedValue, params: Partial): Promise { - const { selectedContainer, showPrevious, selectedPodId } = logTabData.get(); - const pod = this.dependencies.getPodById(selectedPodId); + private async loadLogs(computedPod: IComputedValue, logTabData: IComputedValue, params: Partial): Promise { + await when(() => Boolean(computedPod.get() && logTabData.get()), { timeout: 5_000 }); + + const { selectedContainer, showPrevious } = logTabData.get(); + const pod = computedPod.get(); const namespace = pod.getNs(); const name = pod.getName(); @@ -135,6 +136,10 @@ export class LogStore { return this.getLogs(tabId).length; } + areLogsPresent(tabId: TabId): boolean { + return !this.podLogs.has(tabId); + } + getLogs(tabId: TabId): string[]{ return this.podLogs.get(tabId) ?? []; } @@ -201,9 +206,9 @@ export class LogStore { this.podLogs.delete(tabId); } - reload(tabId: TabId, logTabData: IComputedValue): Promise { + reload(tabId: TabId, computedPod: IComputedValue, logTabData: IComputedValue): Promise { this.clearLogs(tabId); - return this.load(tabId, logTabData); + return this.load(tabId, computedPod, logTabData); } } diff --git a/src/renderer/kube-watch-api/kube-watch-api.ts b/src/renderer/kube-watch-api/kube-watch-api.ts index 4b9a802723..f38ffcec1c 100644 --- a/src/renderer/kube-watch-api/kube-watch-api.ts +++ b/src/renderer/kube-watch-api/kube-watch-api.ts @@ -27,7 +27,6 @@ class WrappedAbortController extends AbortController { interface SubscribeStoreParams { store: KubeObjectStore; parent: AbortController; - watchChanges: boolean; namespaces: string[]; onLoadFailure?: (err: any) => void; } @@ -75,7 +74,7 @@ export interface KubeWatchSubscribeStoreOptions { /** * A function that is called when listing fails. If set then blocks errors - * being rejected with + * from rejecting promises */ onLoadFailure?: (err: any) => void; } @@ -89,12 +88,16 @@ export class KubeWatchApi { constructor(private dependencies: Dependencies) {} - private subscribeStore({ store, parent, watchChanges, namespaces, onLoadFailure }: SubscribeStoreParams): Disposer { - if (this.#watch.inc(store) > 1) { + private subscribeStore({ store, parent, namespaces, onLoadFailure }: SubscribeStoreParams): Disposer { + const isNamespaceFilterWatch = !namespaces; + + if (isNamespaceFilterWatch && this.#watch.inc(store) > 1) { // don't load or subscribe to a store more than once return () => this.#watch.dec(store); } + namespaces ??= this.dependencies.clusterFrameContext?.contextNamespaces ?? []; + let childController = new WrappedAbortController(parent); const unsubscribe = disposer(); @@ -117,7 +120,7 @@ export class KubeWatchApi { */ loadThenSubscribe(namespaces).catch(noop); - const cancelReloading = watchChanges + const cancelReloading = isNamespaceFilterWatch && store.api.isNamespaced ? reaction( // Note: must slice because reaction won't fire if it isn't there () => [this.dependencies.clusterFrameContext.contextNamespaces.slice(), this.dependencies.clusterFrameContext.hasSelectedAll] as const, @@ -141,7 +144,7 @@ export class KubeWatchApi { : noop; // don't watch namespaces if namespaces were provided return () => { - if (this.#watch.dec(store) === 0) { + if (isNamespaceFilterWatch && this.#watch.dec(store) === 0) { // only stop the subcribe if this is the last one cancelReloading(); childController.abort(); @@ -156,8 +159,7 @@ export class KubeWatchApi { ...stores.map(store => this.subscribeStore({ store, parent, - watchChanges: !namespaces && store.api.isNamespaced, - namespaces: namespaces ?? this.dependencies.clusterFrameContext?.contextNamespaces ?? [], + namespaces, onLoadFailure, })), );