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

Fix pod logs not always showing on reconnect to cluster

- Pod logs are now independent of the namespace select filter

- The logs list now shows a spinner while loading

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-01-26 16:50:23 -05:00
parent fa72f28fc5
commit 37ddf6aa44
9 changed files with 79 additions and 33 deletions

View File

@ -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;

View File

@ -19,6 +19,7 @@ import { array, boundMethod, cssNames } from "../../../utils";
import { VirtualList } from "../../virtual-list"; import { VirtualList } from "../../virtual-list";
import { ToBottom } from "./to-bottom"; import { ToBottom } from "./to-bottom";
import type { LogTabViewModel } from "../logs/logs-view-model"; import type { LogTabViewModel } from "../logs/logs-view-model";
import { Spinner } from "../../spinner";
export interface LogListProps { export interface LogListProps {
model: LogTabViewModel; model: LogTabViewModel;
@ -210,10 +211,18 @@ export class LogList extends React.Component<LogListProps> {
}; };
render() { render() {
if (this.props.model.isLoading.get()) {
return (
<div className="LogList flex box grow align-center justify-center">
<Spinner />
</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">
There are no logs available for container There are no logs available for container {this.props.model.logTabData.get()?.selectedContainer}
</div> </div>
); );
} }

View File

@ -4,6 +4,7 @@
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import { bind } from "../../../utils"; import { bind } from "../../../utils";
import type { LogStore } from "./store"; import type { LogStore } from "./store";
import logStoreInjectable from "./store.injectable"; import logStoreInjectable from "./store.injectable";
@ -13,8 +14,8 @@ interface Dependencies {
logStore: LogStore; logStore: LogStore;
} }
function loadLogs({ logStore }: Dependencies, tabId: string, logTabData: IComputedValue<LogTabData>): Promise<void> { function loadLogs({ logStore }: Dependencies, tabId: string, pod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
return logStore.load(tabId, logTabData); return logStore.load(tabId, pod, logTabData);
} }
const loadLogsInjectable = getInjectable({ const loadLogsInjectable = getInjectable({

View File

@ -16,6 +16,7 @@ import stopLoadingLogsInjectable from "./stop-loading-logs.injectable";
import { podsStore } from "../../+workloads-pods/pods.store"; import { podsStore } from "../../+workloads-pods/pods.store";
import createSearchStoreInjectable from "../../../search-store/create-search-store.injectable"; import createSearchStoreInjectable from "../../../search-store/create-search-store.injectable";
import renameTabInjectable from "../dock/rename-tab.injectable"; import renameTabInjectable from "../dock/rename-tab.injectable";
import areLogsPresentInjectable from "./are-logs-present.injectable";
export interface InstantiateArgs { export interface InstantiateArgs {
tabId: TabId; tabId: TabId;
@ -32,6 +33,7 @@ const logsViewModelInjectable = getInjectable({
loadLogs: di.inject(loadLogsInjectable), loadLogs: di.inject(loadLogsInjectable),
renameTab: di.inject(renameTabInjectable), renameTab: di.inject(renameTabInjectable),
stopLoadingLogs: di.inject(stopLoadingLogsInjectable), stopLoadingLogs: di.inject(stopLoadingLogsInjectable),
areLogsPresent: di.inject(areLogsPresentInjectable),
getPodById: id => podsStore.getById(id), getPodById: id => podsStore.getById(id),
getPodsByOwnerId: id => podsStore.getPodsByOwnerId(id), getPodsByOwnerId: id => podsStore.getPodsByOwnerId(id),
createSearchStore: di.inject(createSearchStoreInjectable), createSearchStore: di.inject(createSearchStoreInjectable),

View File

@ -14,18 +14,20 @@ export interface LogTabViewModelDependencies {
getTimestampSplitLogs: (tabId: TabId) => [string, string][]; getTimestampSplitLogs: (tabId: TabId) => [string, string][];
getLogTabData: (tabId: TabId) => LogTabData; getLogTabData: (tabId: TabId) => LogTabData;
setLogTabData: (tabId: TabId, data: LogTabData) => void; setLogTabData: (tabId: TabId, data: LogTabData) => void;
loadLogs: (tabId: TabId, logTabData: IComputedValue<LogTabData>) => Promise<void>; loadLogs: (tabId: TabId, pod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>) => Promise<void>;
reloadLogs: (tabId: TabId, logTabData: IComputedValue<LogTabData>) => Promise<void>; reloadLogs: (tabId: TabId, pod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>) => Promise<void>;
renameTab: (tabId: TabId, title: string) => void; renameTab: (tabId: TabId, title: string) => void;
stopLoadingLogs: (tabId: TabId) => void; stopLoadingLogs: (tabId: TabId) => void;
getPodById: (id: string) => Pod | undefined; getPodById: (id: string) => Pod | undefined;
getPodsByOwnerId: (id: string) => Pod[]; getPodsByOwnerId: (id: string) => Pod[];
areLogsPresent: (tabId: TabId) => boolean;
createSearchStore: () => SearchStore; createSearchStore: () => SearchStore;
} }
export class LogTabViewModel { export class LogTabViewModel {
constructor(protected readonly tabId: TabId, private readonly dependencies: LogTabViewModelDependencies) {} 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 logs = computed(() => this.dependencies.getLogs(this.tabId));
readonly logsWithoutTimestamps = computed(() => this.dependencies.getLogsWithoutTimestamps(this.tabId)); readonly logsWithoutTimestamps = computed(() => this.dependencies.getLogsWithoutTimestamps(this.tabId));
readonly timestampSplitLogs = computed(() => this.dependencies.getTimestampSplitLogs(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 }); this.dependencies.setLogTabData(this.tabId, { ...this.logTabData.get(), ...partialData });
}; };
loadLogs = () => this.dependencies.loadLogs(this.tabId, this.logTabData); loadLogs = () => this.dependencies.loadLogs(this.tabId, this.pod, this.logTabData);
reloadLogs = () => this.dependencies.reloadLogs(this.tabId, this.logTabData); reloadLogs = () => this.dependencies.reloadLogs(this.tabId, this.pod, this.logTabData);
renameTab = (title: string) => this.dependencies.renameTab(this.tabId, title); renameTab = (title: string) => this.dependencies.renameTab(this.tabId, title);
stopLoadingLogs = () => this.dependencies.stopLoadingLogs(this.tabId); stopLoadingLogs = () => this.dependencies.stopLoadingLogs(this.tabId);
} }

View File

@ -4,6 +4,7 @@
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import { bind } from "../../../utils"; import { bind } from "../../../utils";
import type { LogStore } from "./store"; import type { LogStore } from "./store";
import logStoreInjectable from "./store.injectable"; import logStoreInjectable from "./store.injectable";
@ -13,8 +14,8 @@ interface Dependencies {
logStore: LogStore; logStore: LogStore;
} }
function reloadLogs({ logStore }: Dependencies, tabId: string, logTabData: IComputedValue<LogTabData>): Promise<void> { function reloadLogs({ logStore }: Dependencies, tabId: string, pod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
return logStore.reload(tabId, logTabData); return logStore.reload(tabId, pod, logTabData);
} }
const reloadLogsInjectable = getInjectable({ const reloadLogsInjectable = getInjectable({

View File

@ -5,12 +5,10 @@
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { LogStore } from "./store"; import { LogStore } from "./store";
import callForLogsInjectable from "./call-for-logs.injectable"; import callForLogsInjectable from "./call-for-logs.injectable";
import { podsStore } from "../../+workloads-pods/pods.store";
const logStoreInjectable = getInjectable({ const logStoreInjectable = getInjectable({
instantiate: (di) => new LogStore({ instantiate: (di) => new LogStore({
callForLogs: di.inject(callForLogsInjectable), callForLogs: di.inject(callForLogsInjectable),
getPodById: id => podsStore.getById(id),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { observable, IComputedValue } from "mobx"; import { observable, IComputedValue, when } from "mobx";
import type { IPodLogsQuery, Pod } from "../../../../common/k8s-api/endpoints"; import type { IPodLogsQuery, Pod } from "../../../../common/k8s-api/endpoints";
import { getOrInsertWith, interval, IntervalFn } from "../../../utils"; import { getOrInsertWith, interval, IntervalFn } from "../../../utils";
import type { TabId } from "../dock/store"; import type { TabId } from "../dock/store";
@ -14,7 +14,6 @@ type PodLogLine = string;
const logLinesToLoad = 500; const logLinesToLoad = 500;
interface Dependencies { interface Dependencies {
getPodById: (id: string) => Pod | undefined;
callForLogs: ({ namespace, name }: { namespace: string, name: string }, query: IPodLogsQuery) => Promise<string> callForLogs: ({ namespace, name }: { namespace: string, name: string }, query: IPodLogsQuery) => Promise<string>
} }
@ -44,24 +43,24 @@ export class LogStore {
* Also, it handles loading errors, rewriting whole logs with error * Also, it handles loading errors, rewriting whole logs with error
* messages * messages
*/ */
public async load(tabId: TabId, logTabData: IComputedValue<LogTabData>): Promise<void> { public async load(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
try { try {
const logs = await this.loadLogs(logTabData, { const logs = await this.loadLogs(computedPod, logTabData, {
tailLines: this.getLogLines(tabId) + logLinesToLoad, tailLines: this.getLogLines(tabId) + logLinesToLoad,
}); });
this.getRefresher(tabId, logTabData).start(); this.getRefresher(tabId, computedPod, logTabData).start();
this.podLogs.set(tabId, logs); this.podLogs.set(tabId, logs);
} catch (error) { } catch (error) {
this.handlerError(tabId, error); this.handlerError(tabId, error);
} }
} }
private getRefresher(tabId: TabId, logTabData: IComputedValue<LogTabData>): IntervalFn { private getRefresher(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): IntervalFn {
return getOrInsertWith(this.refreshers, tabId, () => ( return getOrInsertWith(this.refreshers, tabId, () => (
interval(10, () => { interval(10, () => {
if (this.podLogs.has(tabId)) { 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. * starting from last line received.
* @param tabId * @param tabId
*/ */
public async loadMore(tabId: TabId, logTabData: IComputedValue<LogTabData>): Promise<void> { public async loadMore(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
if (!this.podLogs.get(tabId).length) { if (!this.podLogs.get(tabId).length) {
return; return;
} }
try { try {
const oldLogs = this.podLogs.get(tabId); const oldLogs = this.podLogs.get(tabId);
const logs = await this.loadLogs(logTabData, { const logs = await this.loadLogs(computedPod, logTabData, {
sinceTime: this.getLastSinceTime(tabId), sinceTime: this.getLastSinceTime(tabId),
}); });
@ -106,9 +105,11 @@ export class LogStore {
* @param params request parameters described in IPodLogsQuery interface * @param params request parameters described in IPodLogsQuery interface
* @returns A fetch request promise * @returns A fetch request promise
*/ */
private async loadLogs(logTabData: IComputedValue<LogTabData>, params: Partial<IPodLogsQuery>): Promise<string[]> { private async loadLogs(computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>, params: Partial<IPodLogsQuery>): Promise<string[]> {
const { selectedContainer, showPrevious, selectedPodId } = logTabData.get(); await when(() => Boolean(computedPod.get() && logTabData.get()), { timeout: 5_000 });
const pod = this.dependencies.getPodById(selectedPodId);
const { selectedContainer, showPrevious } = logTabData.get();
const pod = computedPod.get();
const namespace = pod.getNs(); const namespace = pod.getNs();
const name = pod.getName(); const name = pod.getName();
@ -135,6 +136,10 @@ export class LogStore {
return this.getLogs(tabId).length; return this.getLogs(tabId).length;
} }
areLogsPresent(tabId: TabId): boolean {
return !this.podLogs.has(tabId);
}
getLogs(tabId: TabId): string[]{ getLogs(tabId: TabId): string[]{
return this.podLogs.get(tabId) ?? []; return this.podLogs.get(tabId) ?? [];
} }
@ -201,9 +206,9 @@ export class LogStore {
this.podLogs.delete(tabId); this.podLogs.delete(tabId);
} }
reload(tabId: TabId, logTabData: IComputedValue<LogTabData>): Promise<void> { reload(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
this.clearLogs(tabId); this.clearLogs(tabId);
return this.load(tabId, logTabData); return this.load(tabId, computedPod, logTabData);
} }
} }

View File

@ -27,7 +27,6 @@ class WrappedAbortController extends AbortController {
interface SubscribeStoreParams { interface SubscribeStoreParams {
store: KubeObjectStore<KubeObject>; store: KubeObjectStore<KubeObject>;
parent: AbortController; parent: AbortController;
watchChanges: boolean;
namespaces: string[]; namespaces: string[];
onLoadFailure?: (err: any) => void; onLoadFailure?: (err: any) => void;
} }
@ -75,7 +74,7 @@ export interface KubeWatchSubscribeStoreOptions {
/** /**
* A function that is called when listing fails. If set then blocks errors * A function that is called when listing fails. If set then blocks errors
* being rejected with * from rejecting promises
*/ */
onLoadFailure?: (err: any) => void; onLoadFailure?: (err: any) => void;
} }
@ -89,12 +88,16 @@ export class KubeWatchApi {
constructor(private dependencies: Dependencies) {} constructor(private dependencies: Dependencies) {}
private subscribeStore({ store, parent, watchChanges, namespaces, onLoadFailure }: SubscribeStoreParams): Disposer { private subscribeStore({ store, parent, namespaces, onLoadFailure }: SubscribeStoreParams): Disposer {
if (this.#watch.inc(store) > 1) { const isNamespaceFilterWatch = !namespaces;
if (isNamespaceFilterWatch && this.#watch.inc(store) > 1) {
// don't load or subscribe to a store more than once // don't load or subscribe to a store more than once
return () => this.#watch.dec(store); return () => this.#watch.dec(store);
} }
namespaces ??= this.dependencies.clusterFrameContext?.contextNamespaces ?? [];
let childController = new WrappedAbortController(parent); let childController = new WrappedAbortController(parent);
const unsubscribe = disposer(); const unsubscribe = disposer();
@ -117,7 +120,7 @@ export class KubeWatchApi {
*/ */
loadThenSubscribe(namespaces).catch(noop); loadThenSubscribe(namespaces).catch(noop);
const cancelReloading = watchChanges const cancelReloading = isNamespaceFilterWatch && store.api.isNamespaced
? reaction( ? reaction(
// Note: must slice because reaction won't fire if it isn't there // Note: must slice because reaction won't fire if it isn't there
() => [this.dependencies.clusterFrameContext.contextNamespaces.slice(), this.dependencies.clusterFrameContext.hasSelectedAll] as const, () => [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 : noop; // don't watch namespaces if namespaces were provided
return () => { return () => {
if (this.#watch.dec(store) === 0) { if (isNamespaceFilterWatch && this.#watch.dec(store) === 0) {
// only stop the subcribe if this is the last one // only stop the subcribe if this is the last one
cancelReloading(); cancelReloading();
childController.abort(); childController.abort();
@ -156,8 +159,7 @@ export class KubeWatchApi {
...stores.map(store => this.subscribeStore({ ...stores.map(store => this.subscribeStore({
store, store,
parent, parent,
watchChanges: !namespaces && store.api.isNamespaced, namespaces,
namespaces: namespaces ?? this.dependencies.clusterFrameContext?.contextNamespaces ?? [],
onLoadFailure, onLoadFailure,
})), })),
); );