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:
parent
fa72f28fc5
commit
37ddf6aa44
@ -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;
|
||||
@ -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;
|
||||
@ -210,10 +211,18 @@ export class LogList extends React.Component<LogListProps> {
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.model.isLoading.get()) {
|
||||
return (
|
||||
<div className="LogList flex box grow align-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.logs.length) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<LogTabData>): Promise<void> {
|
||||
return logStore.load(tabId, logTabData);
|
||||
function loadLogs({ logStore }: Dependencies, tabId: string, pod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
|
||||
return logStore.load(tabId, pod, logTabData);
|
||||
}
|
||||
|
||||
const loadLogsInjectable = getInjectable({
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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<LogTabData>) => Promise<void>;
|
||||
reloadLogs: (tabId: TabId, logTabData: IComputedValue<LogTabData>) => Promise<void>;
|
||||
loadLogs: (tabId: TabId, pod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>) => Promise<void>;
|
||||
reloadLogs: (tabId: TabId, pod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>) => Promise<void>;
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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<LogTabData>): Promise<void> {
|
||||
return logStore.reload(tabId, logTabData);
|
||||
function reloadLogs({ logStore }: Dependencies, tabId: string, pod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
|
||||
return logStore.reload(tabId, pod, logTabData);
|
||||
}
|
||||
|
||||
const reloadLogsInjectable = getInjectable({
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<string>
|
||||
}
|
||||
|
||||
@ -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<LogTabData>): Promise<void> {
|
||||
public async load(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
|
||||
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<LogTabData>): IntervalFn {
|
||||
private getRefresher(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): 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<LogTabData>): Promise<void> {
|
||||
public async loadMore(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
|
||||
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<LogTabData>, params: Partial<IPodLogsQuery>): Promise<string[]> {
|
||||
const { selectedContainer, showPrevious, selectedPodId } = logTabData.get();
|
||||
const pod = this.dependencies.getPodById(selectedPodId);
|
||||
private async loadLogs(computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>, params: Partial<IPodLogsQuery>): Promise<string[]> {
|
||||
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<LogTabData>): Promise<void> {
|
||||
reload(tabId: TabId, computedPod: IComputedValue<Pod | undefined>, logTabData: IComputedValue<LogTabData>): Promise<void> {
|
||||
this.clearLogs(tabId);
|
||||
|
||||
return this.load(tabId, logTabData);
|
||||
return this.load(tabId, computedPod, logTabData);
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@ class WrappedAbortController extends AbortController {
|
||||
interface SubscribeStoreParams {
|
||||
store: KubeObjectStore<KubeObject>;
|
||||
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,
|
||||
})),
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user