mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix crash with logs view
- Switch to saving only IDs of pods, owners, and containers instead of whole objects - Add more checks about data integrety Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
fa3708c879
commit
cea7a34014
@ -116,9 +116,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
"version": "17.0.35",
|
"version": "17.0.37",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz",
|
||||||
"integrity": "sha512-r3C8/TJuri/SLZiiwwxQoLAoavaczARfT9up9b4Jr65+ErAUX3MIkU0oMOQnrpfgHme8zIqZLX7O5nnjm5Wayw==",
|
"integrity": "sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
@ -222,9 +222,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"csstype": {
|
"csstype": {
|
||||||
"version": "2.6.18",
|
"version": "2.6.19",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.19.tgz",
|
||||||
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==",
|
"integrity": "sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"debounce-fn": {
|
"debounce-fn": {
|
||||||
|
|||||||
12
extensions/metrics-cluster-feature/package-lock.json
generated
12
extensions/metrics-cluster-feature/package-lock.json
generated
@ -770,9 +770,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
"version": "17.0.35",
|
"version": "17.0.37",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz",
|
||||||
"integrity": "sha512-r3C8/TJuri/SLZiiwwxQoLAoavaczARfT9up9b4Jr65+ErAUX3MIkU0oMOQnrpfgHme8zIqZLX7O5nnjm5Wayw==",
|
"integrity": "sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
@ -876,9 +876,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"csstype": {
|
"csstype": {
|
||||||
"version": "2.6.18",
|
"version": "2.6.19",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.19.tgz",
|
||||||
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==",
|
"integrity": "sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"debounce-fn": {
|
"debounce-fn": {
|
||||||
|
|||||||
12
extensions/node-menu/package-lock.json
generated
12
extensions/node-menu/package-lock.json
generated
@ -736,9 +736,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
"version": "17.0.35",
|
"version": "17.0.37",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz",
|
||||||
"integrity": "sha512-r3C8/TJuri/SLZiiwwxQoLAoavaczARfT9up9b4Jr65+ErAUX3MIkU0oMOQnrpfgHme8zIqZLX7O5nnjm5Wayw==",
|
"integrity": "sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
@ -842,9 +842,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"csstype": {
|
"csstype": {
|
||||||
"version": "2.6.18",
|
"version": "2.6.19",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.18.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.19.tgz",
|
||||||
"integrity": "sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==",
|
"integrity": "sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"debounce-fn": {
|
"debounce-fn": {
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export abstract class BaseStore<T> extends Singleton {
|
|||||||
*/
|
*/
|
||||||
load() {
|
load() {
|
||||||
if (!isTestEnv) {
|
if (!isTestEnv) {
|
||||||
logger.info(`[${kebabCase(this.displayName).toUpperCase()}]: LOADING from ${this.path} ...`);
|
logger.info(`[${kebabCase(this.displayName).toUpperCase()}]: LOADING store with key ${this.params.configName} ...`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.storeConfig = new Config({
|
this.storeConfig = new Config({
|
||||||
|
|||||||
@ -34,7 +34,9 @@ const electronRemote = (() => {
|
|||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
try {
|
try {
|
||||||
return require("@electron/remote");
|
return require("@electron/remote");
|
||||||
} catch {}
|
} catch {
|
||||||
|
// ignore temp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -102,13 +102,13 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed get contextItems(): T[] {
|
@computed get contextItems(): T[] {
|
||||||
const namespaces = this.context?.contextNamespaces ?? [];
|
if (!this.api.isNamespaced) {
|
||||||
|
return this.items;
|
||||||
|
}
|
||||||
|
|
||||||
return this.items.filter(item => {
|
const namespaces = new Set(this.context?.contextNamespaces ?? []);
|
||||||
const itemNamespace = item.getNs();
|
|
||||||
|
|
||||||
return !itemNamespace /* cluster-wide */ || namespaces.includes(itemNamespace);
|
return this.items.filter(item => namespaces.has(item.getNs()));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalCount(): number {
|
getTotalCount(): number {
|
||||||
@ -275,12 +275,18 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
protected mergeItems(partialItems: T[], { merge = true, updateStore = true, sort = true, filter = true } = {}): T[] {
|
protected mergeItems(partialItems: T[], { merge = true, updateStore = true, sort = true, filter = true } = {}): T[] {
|
||||||
let items = partialItems;
|
let items = partialItems;
|
||||||
|
|
||||||
// update existing items
|
|
||||||
if (merge) {
|
if (merge) {
|
||||||
const namespaces = partialItems.map(item => item.getNs());
|
/**
|
||||||
|
* Replace older KubeObjects (by UID) with the new ones.
|
||||||
|
*
|
||||||
|
* This is done by UID and not by namespace (as was previous) because
|
||||||
|
* the store might be loaded (and watching) two different sets of
|
||||||
|
* namespaces at the same time.
|
||||||
|
*/
|
||||||
|
const uids = new Set(partialItems.map(item => item.getId()));
|
||||||
|
|
||||||
items = [
|
items = [
|
||||||
...this.items.filter(existingItem => !namespaces.includes(existingItem.getNs())),
|
...this.items.filter(existingItem => !uids.has(existingItem.getId())),
|
||||||
...partialItems,
|
...partialItems,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,15 @@ export type KubeObjectConstructor<K extends KubeObject> = (new (data: KubeJsonAp
|
|||||||
apiBase?: string;
|
apiBase?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface KubeOwnerReference {
|
||||||
|
apiVersion: string;
|
||||||
|
kind: string;
|
||||||
|
name: string;
|
||||||
|
uid: string;
|
||||||
|
controller: boolean;
|
||||||
|
blockOwnerDeletion: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface KubeObjectMetadata {
|
export interface KubeObjectMetadata {
|
||||||
uid: string;
|
uid: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -53,14 +62,7 @@ export interface KubeObjectMetadata {
|
|||||||
annotations?: {
|
annotations?: {
|
||||||
[annotation: string]: string;
|
[annotation: string]: string;
|
||||||
};
|
};
|
||||||
ownerReferences?: {
|
ownerReferences?: KubeOwnerReference[];
|
||||||
apiVersion: string;
|
|
||||||
kind: string;
|
|
||||||
name: string;
|
|
||||||
uid: string;
|
|
||||||
controller: boolean;
|
|
||||||
blockOwnerDeletion: boolean;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KubeStatusData {
|
export interface KubeStatusData {
|
||||||
|
|||||||
@ -50,14 +50,15 @@ export interface IKubeWatchEvent<T extends KubeJsonApiData> {
|
|||||||
|
|
||||||
export interface KubeWatchSubscribeStoreOptions {
|
export interface KubeWatchSubscribeStoreOptions {
|
||||||
/**
|
/**
|
||||||
* The namespaces to watch
|
* The namespaces to subscribe to. If not specified then will also watch the
|
||||||
|
* changes to set of selected namespaces from `<NamespaceSelectFilter />`
|
||||||
* @default all selected namespaces
|
* @default all selected namespaces
|
||||||
*/
|
*/
|
||||||
namespaces?: string[];
|
namespaces?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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;
|
||||||
}
|
}
|
||||||
@ -116,7 +117,11 @@ export class KubeWatchApi {
|
|||||||
#watch = new WatchCount();
|
#watch = new WatchCount();
|
||||||
|
|
||||||
private subscribeStore({ store, parent, watchChanges, namespaces, onLoadFailure }: SubscribeStoreParams): Disposer {
|
private subscribeStore({ store, parent, watchChanges, namespaces, onLoadFailure }: SubscribeStoreParams): Disposer {
|
||||||
if (this.#watch.inc(store) > 1) {
|
const isNamespaceFilterWatch = !namespaces;
|
||||||
|
|
||||||
|
namespaces ??= KubeWatchApi.context?.contextNamespaces ?? [];
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
@ -167,7 +172,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();
|
||||||
@ -183,7 +188,7 @@ export class KubeWatchApi {
|
|||||||
store,
|
store,
|
||||||
parent,
|
parent,
|
||||||
watchChanges: !namespaces && store.api.isNamespaced,
|
watchChanges: !namespaces && store.api.isNamespaced,
|
||||||
namespaces: namespaces ?? KubeWatchApi.context?.contextNamespaces ?? [],
|
namespaces,
|
||||||
onLoadFailure,
|
onLoadFailure,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|||||||
50
src/common/utils/comparer.ts
Normal file
50
src/common/utils/comparer.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The functions of this module should be used so that typescript doesn't
|
||||||
|
* assume the types of a `reaction` or `autorun` is `any`
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { comparer as _comparer } from "mobx";
|
||||||
|
|
||||||
|
function identity<T>(a: T, b: T): boolean {
|
||||||
|
return _comparer.identity(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultComparer<T>(a: T, b: T): boolean {
|
||||||
|
return _comparer.default(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function structural<T>(a: T, b: T): boolean {
|
||||||
|
return _comparer.structural(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shallow<T>(a: T, b: T): boolean {
|
||||||
|
return _comparer.shallow(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const comparer = {
|
||||||
|
identity,
|
||||||
|
default: defaultComparer,
|
||||||
|
structural,
|
||||||
|
shallow,
|
||||||
|
};
|
||||||
@ -31,6 +31,7 @@ export * from "./autobind";
|
|||||||
export * from "./camelCase";
|
export * from "./camelCase";
|
||||||
export * from "./cloneJson";
|
export * from "./cloneJson";
|
||||||
export * from "./cluster-id-url-parsing";
|
export * from "./cluster-id-url-parsing";
|
||||||
|
export * from "./comparer";
|
||||||
export * from "./convertCpu";
|
export * from "./convertCpu";
|
||||||
export * from "./convertMemory";
|
export * from "./convertMemory";
|
||||||
export * from "./debouncePromise";
|
export * from "./debouncePromise";
|
||||||
@ -49,6 +50,7 @@ export * from "./objects";
|
|||||||
export * from "./openExternal";
|
export * from "./openExternal";
|
||||||
export * from "./paths";
|
export * from "./paths";
|
||||||
export * from "./promise-exec";
|
export * from "./promise-exec";
|
||||||
|
export * from "./reactions";
|
||||||
export * from "./reject-promise";
|
export * from "./reject-promise";
|
||||||
export * from "./singleton";
|
export * from "./singleton";
|
||||||
export * from "./sort-compare";
|
export * from "./sort-compare";
|
||||||
|
|||||||
42
src/common/utils/reactions.ts
Normal file
42
src/common/utils/reactions.ts
Normal file
@ -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 type { IReactionPublic, IReactionOptions, IReactionDisposer } from "mobx";
|
||||||
|
import { reaction } from "mobx";
|
||||||
|
import type { Disposer } from "./disposer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to mobx's builtin `reaction` function but supports returning a
|
||||||
|
* disposer from `effect` that will be cancelled everytime a new reaction is
|
||||||
|
* fired and when the reaction is disposed.
|
||||||
|
*/
|
||||||
|
export function disposingReaction<T, FireImmediately extends boolean = false>(expression: (r: IReactionPublic) => T, effect: (arg: T, prev: FireImmediately extends true ? T | undefined : T, r: IReactionPublic) => Disposer, opts?: IReactionOptions<T, FireImmediately>): IReactionDisposer {
|
||||||
|
let prevDisposer: Disposer;
|
||||||
|
|
||||||
|
const reactionDisposer = reaction<T, FireImmediately>(expression, (arg: T, prev: T, r: IReactionPublic) => {
|
||||||
|
prevDisposer?.();
|
||||||
|
prevDisposer = effect(arg, prev, r);
|
||||||
|
}, opts);
|
||||||
|
|
||||||
|
return Object.assign(() => {
|
||||||
|
reactionDisposer();
|
||||||
|
prevDisposer?.();
|
||||||
|
}, reactionDisposer);
|
||||||
|
}
|
||||||
@ -205,7 +205,7 @@ export class ClusterManager extends Singleton {
|
|||||||
if (error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) {
|
if (error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) {
|
||||||
logger.warn(`${logPrefix} kubeconfig file disappeared`, model);
|
logger.warn(`${logPrefix} kubeconfig file disappeared`, model);
|
||||||
} else {
|
} else {
|
||||||
logger.error(`${logPrefix} failed to add cluster: ${error}`, model);
|
logger.error(`${logPrefix} failed to add cluster: ${error}`, { model, source: entity.metadata.source });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
|
import { action, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||||
import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
|
import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc";
|
||||||
import { ContextHandler } from "./context-handler";
|
import { ContextHandler } from "./context-handler";
|
||||||
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||||
@ -34,7 +34,7 @@ import { DetectorRegistry } from "./cluster-detectors/detector-registry";
|
|||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../common/cluster-types";
|
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../common/cluster-types";
|
||||||
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types";
|
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types";
|
||||||
import { disposer, storedKubeConfigFolder, toJS } from "../common/utils";
|
import { disposer, storedKubeConfigFolder, toJS, comparer } from "../common/utils";
|
||||||
import type { Response } from "request";
|
import type { Response } from "request";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -25,8 +25,7 @@ import { render } from "@testing-library/react";
|
|||||||
import * as selectEvent from "react-select-event";
|
import * as selectEvent from "react-select-event";
|
||||||
|
|
||||||
import { Pod } from "../../../../common/k8s-api/endpoints";
|
import { Pod } from "../../../../common/k8s-api/endpoints";
|
||||||
import { LogResourceSelector } from "../log-resource-selector";
|
import { LogResourceSelector, LogResourceSelectorProps } from "../log-resource-selector";
|
||||||
import type { LogTabData } from "../log-tab.store";
|
|
||||||
import { dockerPod, deploymentPod1 } from "./pod.mock";
|
import { dockerPod, deploymentPod1 } from "./pod.mock";
|
||||||
import { ThemeStore } from "../../../theme.store";
|
import { ThemeStore } from "../../../theme.store";
|
||||||
import { UserStore } from "../../../../common/user-store";
|
import { UserStore } from "../../../../common/user-store";
|
||||||
@ -51,35 +50,26 @@ jest.mock("electron", () => ({
|
|||||||
|
|
||||||
AppPaths.init();
|
AppPaths.init();
|
||||||
|
|
||||||
const getComponent = (tabData: LogTabData) => {
|
const getOnePodTabProps = (): LogResourceSelectorProps => {
|
||||||
return (
|
|
||||||
<LogResourceSelector
|
|
||||||
tabId="tabId"
|
|
||||||
tabData={tabData}
|
|
||||||
save={jest.fn()}
|
|
||||||
reload={jest.fn()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOnePodTabData = (): LogTabData => {
|
|
||||||
const selectedPod = new Pod(dockerPod);
|
const selectedPod = new Pod(dockerPod);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pods: [] as Pod[],
|
pod: selectedPod,
|
||||||
selectedPod,
|
pods: [selectedPod],
|
||||||
selectedContainer: selectedPod.getContainers()[0],
|
tabId: "tabId",
|
||||||
|
selectedContainer: selectedPod.getContainers()[0].name,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFewPodsTabData = (): LogTabData => {
|
const getFewPodsTabProps = (): LogResourceSelectorProps => {
|
||||||
const selectedPod = new Pod(deploymentPod1);
|
const selectedPod = new Pod(deploymentPod1);
|
||||||
const anotherPod = new Pod(dockerPod);
|
const anotherPod = new Pod(dockerPod);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pods: [anotherPod],
|
pod: selectedPod,
|
||||||
selectedPod,
|
pods: [selectedPod, anotherPod],
|
||||||
selectedContainer: selectedPod.getContainers()[0],
|
tabId: "tabId",
|
||||||
|
selectedContainer: selectedPod.getContainers()[0].name,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -99,42 +89,46 @@ describe("<LogResourceSelector />", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders w/o errors", () => {
|
it("renders w/o errors", () => {
|
||||||
const tabData = getOnePodTabData();
|
const props = getOnePodTabProps();
|
||||||
const { container } = render(getComponent(tabData));
|
const { container } = render(<LogResourceSelector {...props} />);
|
||||||
|
|
||||||
expect(container).toBeInstanceOf(HTMLElement);
|
expect(container).toBeInstanceOf(HTMLElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders proper namespace", () => {
|
it("renders proper namespace", () => {
|
||||||
const tabData = getOnePodTabData();
|
const props = getOnePodTabProps();
|
||||||
const { getByTestId } = render(getComponent(tabData));
|
const { getByTestId } = render(<LogResourceSelector {...props} />);
|
||||||
const ns = getByTestId("namespace-badge");
|
const ns = getByTestId("namespace-badge");
|
||||||
|
|
||||||
expect(ns).toHaveTextContent("default");
|
expect(ns).toHaveTextContent("default");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders proper selected items within dropdowns", () => {
|
it("renders proper selected items within dropdowns", () => {
|
||||||
const tabData = getOnePodTabData();
|
const props = getOnePodTabProps();
|
||||||
const { getByText } = render(getComponent(tabData));
|
const { getByText } = render(<LogResourceSelector {...props} />);
|
||||||
|
|
||||||
expect(getByText("dockerExporter")).toBeInTheDocument();
|
expect(getByText("dockerExporter")).toBeInTheDocument();
|
||||||
expect(getByText("docker-exporter")).toBeInTheDocument();
|
expect(getByText("docker-exporter")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders sibling pods in dropdown", () => {
|
it("renders sibling pods in dropdown", () => {
|
||||||
const tabData = getFewPodsTabData();
|
const props = getFewPodsTabProps();
|
||||||
const { container, getByText } = render(getComponent(tabData));
|
const { container, getByText } = render(<LogResourceSelector {...props} />);
|
||||||
const podSelector: HTMLElement = container.querySelector(".pod-selector");
|
const podSelector: HTMLElement = container.querySelector(".pod-selector");
|
||||||
|
|
||||||
selectEvent.openMenu(podSelector);
|
selectEvent.openMenu(podSelector);
|
||||||
|
|
||||||
expect(getByText("dockerExporter")).toBeInTheDocument();
|
expect(getByText("dockerExporter", {
|
||||||
expect(getByText("deploymentPod1")).toBeInTheDocument();
|
selector: ".Select__option",
|
||||||
|
})).toBeInTheDocument();
|
||||||
|
expect(getByText("deploymentPod1", {
|
||||||
|
selector: ".Select__option",
|
||||||
|
})).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders sibling containers in dropdown", () => {
|
it("renders sibling containers in dropdown", () => {
|
||||||
const tabData = getFewPodsTabData();
|
const props = getFewPodsTabProps();
|
||||||
const { getByText, container } = render(getComponent(tabData));
|
const { getByText, container } = render(<LogResourceSelector {...props} />);
|
||||||
const containerSelector: HTMLElement = container.querySelector(".container-selector");
|
const containerSelector: HTMLElement = container.querySelector(".container-selector");
|
||||||
|
|
||||||
selectEvent.openMenu(containerSelector);
|
selectEvent.openMenu(containerSelector);
|
||||||
@ -145,8 +139,8 @@ describe("<LogResourceSelector />", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders pod owner as dropdown title", () => {
|
it("renders pod owner as dropdown title", () => {
|
||||||
const tabData = getFewPodsTabData();
|
const props = getFewPodsTabProps();
|
||||||
const { getByText, container } = render(getComponent(tabData));
|
const { getByText, container } = render(<LogResourceSelector {...props} />);
|
||||||
const podSelector: HTMLElement = container.querySelector(".pod-selector");
|
const podSelector: HTMLElement = container.querySelector(".pod-selector");
|
||||||
|
|
||||||
selectEvent.openMenu(podSelector);
|
selectEvent.openMenu(podSelector);
|
||||||
|
|||||||
@ -19,146 +19,137 @@
|
|||||||
* 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 { podsStore } from "../../+workloads-pods/pods.store";
|
|
||||||
import { UserStore } from "../../../../common/user-store";
|
|
||||||
import { Pod } from "../../../../common/k8s-api/endpoints";
|
import { Pod } from "../../../../common/k8s-api/endpoints";
|
||||||
import { ThemeStore } from "../../../theme.store";
|
import type { TabId } from "../dock.store";
|
||||||
import { dockStore } from "../dock.store";
|
import { DockManager, LogTabStore } from "../log-tab.store";
|
||||||
import { logTabStore } from "../log-tab.store";
|
import { deploymentPod1, dockerPod, noOwnersPod } from "./pod.mock";
|
||||||
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
|
|
||||||
import fse from "fs-extra";
|
|
||||||
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
|
||||||
import { AppPaths } from "../../../../common/app-paths";
|
|
||||||
|
|
||||||
mockWindow();
|
function getMockDockManager(): jest.Mocked<DockManager> {
|
||||||
|
return {
|
||||||
|
renameTab: jest.fn(),
|
||||||
|
createTab: jest.fn(),
|
||||||
|
closeTab: jest.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
jest.mock("electron", () => ({
|
describe("LogTabStore", () => {
|
||||||
app: {
|
describe("createPodTab()", () => {
|
||||||
getVersion: () => "99.99.99",
|
it("throws if data is not an object", () => {
|
||||||
getName: () => "lens",
|
const dockManager = getMockDockManager();
|
||||||
setName: jest.fn(),
|
const logTabStore = new LogTabStore({ autoInit: false }, dockManager);
|
||||||
setPath: jest.fn(),
|
|
||||||
getPath: () => "tmp",
|
|
||||||
getLocale: () => "en",
|
|
||||||
setLoginItemSettings: jest.fn(),
|
|
||||||
},
|
|
||||||
ipcMain: {
|
|
||||||
on: jest.fn(),
|
|
||||||
handle: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
AppPaths.init();
|
expect(() => logTabStore.createPodTab(0 as any)).toThrow(/is not an object/);
|
||||||
|
|
||||||
podsStore.items.push(new Pod(dockerPod));
|
|
||||||
podsStore.items.push(new Pod(deploymentPod1));
|
|
||||||
podsStore.items.push(new Pod(deploymentPod2));
|
|
||||||
|
|
||||||
describe("log tab store", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
UserStore.createInstance();
|
|
||||||
ThemeStore.createInstance();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
logTabStore.reset();
|
|
||||||
dockStore.reset();
|
|
||||||
UserStore.resetInstance();
|
|
||||||
ThemeStore.resetInstance();
|
|
||||||
fse.remove("tmp");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates log tab without sibling pods", () => {
|
|
||||||
const selectedPod = new Pod(dockerPod);
|
|
||||||
const selectedContainer = selectedPod.getAllContainers()[0];
|
|
||||||
|
|
||||||
logTabStore.createPodTab({
|
|
||||||
selectedPod,
|
|
||||||
selectedContainer,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
|
it("throws if data.selectedPod is not an object", () => {
|
||||||
pods: [selectedPod],
|
const dockManager = getMockDockManager();
|
||||||
selectedPod,
|
const logTabStore = new LogTabStore({ autoInit: false }, dockManager);
|
||||||
selectedContainer,
|
|
||||||
showTimestamps: false,
|
expect(() => logTabStore.createPodTab({ selectedPod: 1 as any, selectedContainer: {} as any })).toThrow(/is not an object/);
|
||||||
previous: false,
|
});
|
||||||
|
|
||||||
|
it("throws if data.selectedContainer is not an object", () => {
|
||||||
|
const dockManager = getMockDockManager();
|
||||||
|
const logTabStore = new LogTabStore({ autoInit: false }, dockManager);
|
||||||
|
|
||||||
|
expect(() => logTabStore.createPodTab({ selectedContainer: 1 as any, selectedPod: {} as any })).toThrow(/is not an object/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not throw if selectedPod has no owner refs", () => {
|
||||||
|
const dockManager = getMockDockManager();
|
||||||
|
const logTabStore = new LogTabStore({ autoInit: false }, dockManager);
|
||||||
|
const pod = new Pod(noOwnersPod);
|
||||||
|
|
||||||
|
expect(() => logTabStore.createPodTab({ selectedContainer: {} as any, selectedPod: pod })).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a TabId if created sucessfully", () => {
|
||||||
|
const dockManager = getMockDockManager();
|
||||||
|
const logTabStore = new LogTabStore({ autoInit: false }, dockManager);
|
||||||
|
const pod = new Pod(dockerPod);
|
||||||
|
|
||||||
|
expect(typeof logTabStore.createPodTab({ selectedContainer: { name: "docker-exporter" } as any, selectedPod: pod })).toBe("string");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates log tab with sibling pods", () => {
|
describe("changeSelectedPod()", () => {
|
||||||
const selectedPod = new Pod(deploymentPod1);
|
let dockManager: jest.Mocked<DockManager>;
|
||||||
const siblingPod = new Pod(deploymentPod2);
|
let logTabStore: LogTabStore;
|
||||||
const selectedContainer = selectedPod.getInitContainers()[0];
|
let tabId: TabId;
|
||||||
|
const pod = new Pod(dockerPod);
|
||||||
|
|
||||||
logTabStore.createPodTab({
|
beforeEach(() => {
|
||||||
selectedPod,
|
dockManager = getMockDockManager();
|
||||||
selectedContainer,
|
logTabStore = new LogTabStore({ autoInit: false }, dockManager);
|
||||||
|
tabId = logTabStore.createPodTab({ selectedContainer: { name: "docker-exporter" } as any, selectedPod: pod });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
|
it("should work as expected", () => {
|
||||||
pods: [selectedPod, siblingPod],
|
const newPod = new Pod(deploymentPod1);
|
||||||
selectedPod,
|
|
||||||
selectedContainer,
|
logTabStore.changeSelectedPod(tabId, newPod);
|
||||||
showTimestamps: false,
|
|
||||||
previous: false,
|
expect(logTabStore.getData(tabId)).toMatchObject({
|
||||||
|
selectedPod: newPod.getId(),
|
||||||
|
selectedContainer: newPod.getContainers()[0].name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should rename the tab", () => {
|
||||||
|
const newPod = new Pod(deploymentPod1);
|
||||||
|
|
||||||
|
logTabStore.changeSelectedPod(tabId, newPod);
|
||||||
|
expect(dockManager.renameTab).toBeCalledWith(tabId, "Pod Logs: deploymentPod1");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes item from pods list if pod deleted from store", () => {
|
describe("getData()", () => {
|
||||||
const selectedPod = new Pod(deploymentPod1);
|
let dockManager: jest.Mocked<DockManager>;
|
||||||
const selectedContainer = selectedPod.getInitContainers()[0];
|
let logTabStore: LogTabStore;
|
||||||
|
let tabId: TabId;
|
||||||
|
const pod = new Pod(dockerPod);
|
||||||
|
|
||||||
logTabStore.createPodTab({
|
beforeEach(() => {
|
||||||
selectedPod,
|
dockManager = getMockDockManager();
|
||||||
selectedContainer,
|
logTabStore = new LogTabStore({ autoInit: false }, dockManager);
|
||||||
|
tabId = logTabStore.createPodTab({ selectedContainer: { name: "docker-exporter" } as any, selectedPod: pod });
|
||||||
});
|
});
|
||||||
|
|
||||||
podsStore.items.pop();
|
it("should return data if created", () => {
|
||||||
|
expect(logTabStore.getData(tabId)).toMatchObject({
|
||||||
|
selectedPod: pod.getId(),
|
||||||
|
selectedContainer: "docker-exporter",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
|
it("should return undefined for unknown tab ID", () => {
|
||||||
pods: [selectedPod],
|
expect(logTabStore.getData("foo")).toBeUndefined();
|
||||||
selectedPod,
|
|
||||||
selectedContainer,
|
|
||||||
showTimestamps: false,
|
|
||||||
previous: false,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds item into pods list if new sibling pod added to store", () => {
|
describe("setData()", () => {
|
||||||
const selectedPod = new Pod(deploymentPod1);
|
let dockManager: jest.Mocked<DockManager>;
|
||||||
const selectedContainer = selectedPod.getInitContainers()[0];
|
let logTabStore: LogTabStore;
|
||||||
|
|
||||||
logTabStore.createPodTab({
|
beforeEach(() => {
|
||||||
selectedPod,
|
dockManager = getMockDockManager();
|
||||||
selectedContainer,
|
logTabStore = new LogTabStore({ autoInit: false }, dockManager);
|
||||||
});
|
});
|
||||||
|
|
||||||
podsStore.items.push(new Pod(deploymentPod3));
|
it("should throw an error on invalid data", () => {
|
||||||
|
expect(() => logTabStore.setData("foo", 7 as any)).toThrowError();
|
||||||
expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
|
|
||||||
pods: [selectedPod, deploymentPod3],
|
|
||||||
selectedPod,
|
|
||||||
selectedContainer,
|
|
||||||
showTimestamps: false,
|
|
||||||
previous: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// FIXME: this is failed when it's not .only == depends on something above
|
|
||||||
it.only("closes tab if no pods left in store", async () => {
|
|
||||||
const selectedPod = new Pod(deploymentPod1);
|
|
||||||
const selectedContainer = selectedPod.getInitContainers()[0];
|
|
||||||
|
|
||||||
const id = logTabStore.createPodTab({
|
|
||||||
selectedPod,
|
|
||||||
selectedContainer,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
podsStore.items.clear();
|
it("should not throw an error on valid data", () => {
|
||||||
|
expect(() => logTabStore.setData("foo", {
|
||||||
expect(logTabStore.getData(dockStore.selectedTabId)).toBeUndefined();
|
namespace: "foobar",
|
||||||
expect(logTabStore.getData(id)).toBeUndefined();
|
previous: false,
|
||||||
expect(dockStore.getTabById(id)).toBeUndefined();
|
selectedPod: "blat",
|
||||||
|
showTimestamps: false,
|
||||||
|
podsOwner: "bar",
|
||||||
|
selectedContainer: "bat",
|
||||||
|
})).not.toThrowError();
|
||||||
|
expect(logTabStore.getData("foo")).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,9 +19,11 @@
|
|||||||
* 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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const dockerPod = {
|
import type { IPodContainer } from "../../../../common/k8s-api/endpoints";
|
||||||
|
|
||||||
|
export const noOwnersPod = {
|
||||||
apiVersion: "v1",
|
apiVersion: "v1",
|
||||||
kind: "dummy",
|
kind: "Pod",
|
||||||
metadata: {
|
metadata: {
|
||||||
uid: "dockerExporter",
|
uid: "dockerExporter",
|
||||||
name: "dockerExporter",
|
name: "dockerExporter",
|
||||||
@ -30,7 +32,48 @@ export const dockerPod = {
|
|||||||
namespace: "default",
|
namespace: "default",
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
initContainers: [] as any,
|
initContainers: [] as IPodContainer[],
|
||||||
|
containers: [
|
||||||
|
{
|
||||||
|
name: "docker-exporter",
|
||||||
|
image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
|
||||||
|
imagePullPolicy: "pull",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
serviceAccountName: "dummy",
|
||||||
|
serviceAccount: "dummy",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "Running",
|
||||||
|
conditions: [{
|
||||||
|
type: "Running",
|
||||||
|
status: "Running",
|
||||||
|
lastProbeTime: 1,
|
||||||
|
lastTransitionTime: "Some time",
|
||||||
|
}],
|
||||||
|
hostIP: "dummy",
|
||||||
|
podIP: "dummy",
|
||||||
|
startTime: "dummy",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dockerPod = {
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "Pod",
|
||||||
|
metadata: {
|
||||||
|
uid: "dockerExporter",
|
||||||
|
name: "dockerExporter",
|
||||||
|
creationTimestamp: "dummy",
|
||||||
|
resourceVersion: "dummy",
|
||||||
|
namespace: "default",
|
||||||
|
ownerReferences: [
|
||||||
|
{
|
||||||
|
uid: "dockerExporterOwner",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
initContainers: [] as IPodContainer[],
|
||||||
containers: [
|
containers: [
|
||||||
{
|
{
|
||||||
name: "docker-exporter",
|
name: "docker-exporter",
|
||||||
|
|||||||
@ -19,55 +19,80 @@
|
|||||||
* 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 { autorun, observable, reaction } from "mobx";
|
import { action, autorun, observable, reaction } from "mobx";
|
||||||
import { autoBind, createStorage, StorageHelper, toJS } from "../../utils";
|
import logger from "../../../common/logger";
|
||||||
|
import { autoBind, createStorage, noop, StorageHelper, toJS } from "../../utils";
|
||||||
import { dockStore, TabId } from "./dock.store";
|
import { dockStore, TabId } from "./dock.store";
|
||||||
|
|
||||||
export interface DockTabStoreOptions {
|
export interface DockTabStoreOptions<T> {
|
||||||
autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events, default: true
|
/**
|
||||||
storageKey?: string; // save data to persistent storage under the key
|
* load data from storage when `storageKey` is provided and bind events
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
autoInit?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* save data to persistent storage under the key
|
||||||
|
*/
|
||||||
|
storageKey?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function to call for validating values. It should `throw` if an error is present
|
||||||
|
*/
|
||||||
|
validator?: (value: T) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DockTabStorageState<T> = Record<TabId, T>;
|
type PartialObject<T> = T extends object ? Partial<T> : never;
|
||||||
|
|
||||||
export class DockTabStore<T> {
|
export class DockTabStore<T> {
|
||||||
protected storage?: StorageHelper<DockTabStorageState<T>>;
|
protected storage?: StorageHelper<Record<TabId, T>>;
|
||||||
protected data = observable.map<TabId, T>();
|
protected data = observable.map<TabId, T>();
|
||||||
|
protected validator: (value: T) => void;
|
||||||
|
|
||||||
constructor(protected options: DockTabStoreOptions = {}) {
|
constructor({ autoInit = true, storageKey, validator = noop }: DockTabStoreOptions<T> = {}) {
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
|
|
||||||
this.options = {
|
this.validator = validator;
|
||||||
autoInit: true,
|
|
||||||
...this.options,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.options.autoInit) {
|
if (autoInit) {
|
||||||
this.init();
|
this.init(storageKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected init() {
|
protected init(storageKey: string | undefined) {
|
||||||
const { storageKey } = this.options;
|
|
||||||
|
|
||||||
// auto-save to local-storage
|
// auto-save to local-storage
|
||||||
if (storageKey) {
|
if (storageKey) {
|
||||||
this.storage = createStorage(storageKey, {});
|
this.storage = createStorage(storageKey, {});
|
||||||
this.storage.whenReady.then(() => {
|
this.storage.whenReady.then(action(() => {
|
||||||
this.data.replace(this.storage.get());
|
for (const [tabId, value] of Object.entries(this.storage.get())) {
|
||||||
reaction(() => this.toJSON(), data => this.storage.set(data));
|
try {
|
||||||
});
|
this.setData(tabId, value);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[DOCK-TAB-STORE-${storageKey}]: data for ${tabId} was invalid, skipping`, error);
|
||||||
|
dockStore.closeTab(tabId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reaction(
|
||||||
|
() => this.toJSON(),
|
||||||
|
data => this.storage.set(data),
|
||||||
|
{
|
||||||
|
// fireImmediately so that invalid data is removed from the store
|
||||||
|
fireImmediately: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear data for closed tabs
|
// clear data for closed tabs
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const currentTabs = dockStore.tabs.map(tab => tab.id);
|
const currentTabs = new Set(dockStore.tabs.map(tab => tab.id));
|
||||||
|
|
||||||
Array.from(this.data.keys()).forEach(tabId => {
|
for (const tabId in this.data) {
|
||||||
if (!currentTabs.includes(tabId)) {
|
if (!currentTabs.has(tabId)) {
|
||||||
this.clearData(tabId);
|
this.clearData(tabId);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +100,7 @@ export class DockTabStore<T> {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected toJSON(): DockTabStorageState<T> {
|
protected toJSON(): Record<TabId, T> {
|
||||||
const deepCopy = toJS(this.data);
|
const deepCopy = toJS(this.data);
|
||||||
|
|
||||||
deepCopy.forEach((tabData, key) => {
|
deepCopy.forEach((tabData, key) => {
|
||||||
@ -94,9 +119,21 @@ export class DockTabStore<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setData(tabId: TabId, data: T) {
|
setData(tabId: TabId, data: T) {
|
||||||
|
this.validator(data);
|
||||||
this.data.set(tabId, data);
|
this.data.set(tabId, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a partial update for the dock tab data.
|
||||||
|
*
|
||||||
|
* NOTE: only supported for object types
|
||||||
|
* @param tabId The ID of the tab to merge data with
|
||||||
|
* @param data The partial value of the data
|
||||||
|
*/
|
||||||
|
mergeData(tabId: TabId, data: PartialObject<T>) {
|
||||||
|
this.setData(tabId, { ...this.getData(tabId), ...data });
|
||||||
|
}
|
||||||
|
|
||||||
clearData(tabId: TabId) {
|
clearData(tabId: TabId) {
|
||||||
this.data.delete(tabId);
|
this.data.delete(tabId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,8 +43,8 @@ export class EditResourceStore extends DockTabStore<EditingResource> {
|
|||||||
autoBind(this);
|
autoBind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async init() {
|
protected async init(storageKey: string | undefined) {
|
||||||
super.init();
|
super.init(storageKey);
|
||||||
await this.storage.whenReady;
|
await this.storage.whenReady;
|
||||||
|
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
|
|||||||
@ -24,45 +24,37 @@ import "./log-controls.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
import { Pod } from "../../../common/k8s-api/endpoints";
|
import type { Pod } from "../../../common/k8s-api/endpoints";
|
||||||
import { cssNames, saveFileDialog } from "../../utils";
|
import { cssNames, saveFileDialog } from "../../utils";
|
||||||
import { logStore } from "./log.store";
|
import { logStore } from "./log.store";
|
||||||
import { Checkbox } from "../checkbox";
|
import { Checkbox } from "../checkbox";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import type { LogTabData } from "./log-tab.store";
|
import type { TabId } from "./dock.store";
|
||||||
|
import { logTabStore } from "./log-tab.store";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tabData?: LogTabData
|
pod: Pod;
|
||||||
logs: string[]
|
tabId: TabId;
|
||||||
save: (data: Partial<LogTabData>) => void
|
preferences: {
|
||||||
reload: () => void
|
showTimestamps: boolean;
|
||||||
|
previous: boolean;
|
||||||
|
};
|
||||||
|
logs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LogControls = observer((props: Props) => {
|
export const LogControls = observer(({ pod, tabId, preferences, logs }: Props) => {
|
||||||
const { tabData, save, reload, logs } = props;
|
const since = logStore.getFirstTime(tabId);
|
||||||
|
|
||||||
if (!tabData) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { showTimestamps, previous } = tabData;
|
|
||||||
const since = logs.length ? logStore.getTimestamps(logs[0]) : null;
|
|
||||||
const pod = new Pod(tabData.selectedPod);
|
|
||||||
|
|
||||||
const toggleTimestamps = () => {
|
const toggleTimestamps = () => {
|
||||||
save({ showTimestamps: !showTimestamps });
|
logTabStore.mergeData(tabId, { showTimestamps: !preferences.showTimestamps });
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePrevious = () => {
|
const togglePrevious = () => {
|
||||||
save({ previous: !previous });
|
logTabStore.mergeData(tabId, { previous: !preferences.previous });
|
||||||
reload();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadLogs = () => {
|
const downloadLogs = () => {
|
||||||
const fileName = pod.getName();
|
saveFileDialog(`${pod.getName()}.log`, logs.join("\n"), "text/plain");
|
||||||
const logsToDownload = showTimestamps ? logs : logStore.logsWithoutTimestamps;
|
|
||||||
|
|
||||||
saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -70,21 +62,20 @@ export const LogControls = observer((props: Props) => {
|
|||||||
<div className="time-range">
|
<div className="time-range">
|
||||||
{since && (
|
{since && (
|
||||||
<span>
|
<span>
|
||||||
Logs from{" "}
|
Logs from <b>{since}</b>
|
||||||
<b>{new Date(since[0]).toLocaleString()}</b>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gaps align-center">
|
<div className="flex gaps align-center">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Show timestamps"
|
label="Show timestamps"
|
||||||
value={showTimestamps}
|
value={preferences.showTimestamps}
|
||||||
onChange={toggleTimestamps}
|
onChange={toggleTimestamps}
|
||||||
className="show-timestamps"
|
className="show-timestamps"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Show previous terminated container"
|
label="Show previous terminated container"
|
||||||
value={previous}
|
value={preferences.previous}
|
||||||
onChange={togglePrevious}
|
onChange={togglePrevious}
|
||||||
className="show-previous"
|
className="show-previous"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -25,25 +25,20 @@ import React from "react";
|
|||||||
import AnsiUp from "ansi_up";
|
import AnsiUp from "ansi_up";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
import { action, computed, observable, makeObservable, reaction } from "mobx";
|
import { action, observable, makeObservable, reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import moment from "moment-timezone";
|
|
||||||
import type { Align, ListOnScrollProps } from "react-window";
|
import type { Align, ListOnScrollProps } from "react-window";
|
||||||
|
|
||||||
import { SearchStore, searchStore } from "../../../common/search-store";
|
import { SearchStore, searchStore } from "../../../common/search-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 { logStore } from "./log.store";
|
|
||||||
import { logTabStore } from "./log-tab.store";
|
|
||||||
import { ToBottom } from "./to-bottom";
|
import { ToBottom } from "./to-bottom";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logs: string[]
|
logs: string[]
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
load: () => void
|
load: () => void
|
||||||
id: string
|
selectedContainer: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorConverter = new AnsiUp();
|
const colorConverter = new AnsiUp();
|
||||||
@ -64,9 +59,11 @@ export class LogList extends React.Component<Props> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => this.props.logs, this.onLogsInitialLoad),
|
reaction(() => this.props.logs, (logs, prevLogs) => {
|
||||||
reaction(() => this.props.logs, this.onLogsUpdate),
|
this.onLogsInitialLoad(logs, prevLogs);
|
||||||
reaction(() => this.props.logs, this.onUserScrolledUp),
|
this.onLogsUpdate();
|
||||||
|
this.onUserScrolledUp(logs, prevLogs);
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,10 +85,14 @@ export class LogList extends React.Component<Props> {
|
|||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
onUserScrolledUp(logs: string[], prevLogs: string[]) {
|
onUserScrolledUp(logs: string[], prevLogs: string[]) {
|
||||||
if (!this.virtualListDiv.current) return;
|
const { current } = this.virtualListDiv;
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newLogsAdded = prevLogs.length < logs.length;
|
const newLogsAdded = prevLogs.length < logs.length;
|
||||||
const scrolledToBeginning = this.virtualListDiv.current.scrollTop === 0;
|
const scrolledToBeginning = current.scrollTop === 0;
|
||||||
|
|
||||||
if (newLogsAdded && scrolledToBeginning) {
|
if (newLogsAdded && scrolledToBeginning) {
|
||||||
const firstLineContents = prevLogs[0];
|
const firstLineContents = prevLogs[0];
|
||||||
@ -103,22 +104,6 @@ export class LogList extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns logs with or without timestamps regarding to showTimestamps prop
|
|
||||||
*/
|
|
||||||
@computed
|
|
||||||
get logs() {
|
|
||||||
const showTimestamps = logTabStore.getData(this.props.id)?.showTimestamps;
|
|
||||||
|
|
||||||
if (!showTimestamps) {
|
|
||||||
return logStore.logsWithoutTimestamps;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.props.logs
|
|
||||||
.map(log => logStore.splitOutTimestamp(log))
|
|
||||||
.map(([logTimestamp, log]) => (`${logTimestamp && moment.tz(logTimestamp, UserStore.getInstance().localeTimezone).format()}${log}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if JumpToBottom button should be visible and sets its observable
|
* Checks if JumpToBottom button should be visible and sets its observable
|
||||||
* @param props Scrolling props from virtual list core
|
* @param props Scrolling props from virtual list core
|
||||||
@ -142,7 +127,13 @@ export class LogList extends React.Component<Props> {
|
|||||||
*/
|
*/
|
||||||
@action
|
@action
|
||||||
setLastLineVisibility = (props: ListOnScrollProps) => {
|
setLastLineVisibility = (props: ListOnScrollProps) => {
|
||||||
const { scrollHeight, clientHeight } = this.virtualListDiv.current;
|
const { current } = this.virtualListDiv;
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { scrollHeight, clientHeight } = current;
|
||||||
const { scrollOffset } = props;
|
const { scrollOffset } = props;
|
||||||
|
|
||||||
this.isLastLineVisible = (clientHeight + scrollOffset) === scrollHeight;
|
this.isLastLineVisible = (clientHeight + scrollOffset) === scrollHeight;
|
||||||
@ -152,34 +143,38 @@ export class LogList extends React.Component<Props> {
|
|||||||
* Check if user scrolled to top and new logs should be loaded
|
* Check if user scrolled to top and new logs should be loaded
|
||||||
* @param props Scrolling props from virtual list core
|
* @param props Scrolling props from virtual list core
|
||||||
*/
|
*/
|
||||||
checkLoadIntent = (props: ListOnScrollProps) => {
|
checkLoadIntent = ({ scrollOffset }: ListOnScrollProps) => {
|
||||||
const { scrollOffset } = props;
|
|
||||||
|
|
||||||
if (scrollOffset === 0) {
|
if (scrollOffset === 0) {
|
||||||
this.props.load();
|
this.props.load();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
scrollToBottom = () => {
|
scrollToItem = (index: number, align: Align) => {
|
||||||
if (!this.virtualListDiv.current) return;
|
this.virtualListRef.current?.scrollToItem(index, align);
|
||||||
this.virtualListDiv.current.scrollTop = this.virtualListDiv.current.scrollHeight;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
scrollToItem = (index: number, align: Align) => {
|
scrollToBottom = () => {
|
||||||
this.virtualListRef.current.scrollToItem(index, align);
|
const { current } = this.virtualListDiv;
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
current.scrollTop = current.scrollHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
onScroll = (props: ListOnScrollProps) => {
|
onScroll = (props: ListOnScrollProps) => {
|
||||||
this.isLastLineVisible = false;
|
this.isLastLineVisible = false;
|
||||||
|
this.setButtonVisibility(props);
|
||||||
|
this.setLastLineVisibility(props);
|
||||||
this.onScrollDebounced(props);
|
this.onScrollDebounced(props);
|
||||||
};
|
};
|
||||||
|
|
||||||
onScrollDebounced = debounce((props: ListOnScrollProps) => {
|
onScrollDebounced = debounce((props: ListOnScrollProps) => {
|
||||||
if (!this.virtualListDiv.current) return;
|
|
||||||
this.setButtonVisibility(props);
|
|
||||||
this.setLastLineVisibility(props);
|
|
||||||
this.checkLoadIntent(props);
|
this.checkLoadIntent(props);
|
||||||
}, 700); // Increasing performance and giving some time for virtual list to settle down
|
}, 700, {
|
||||||
|
leading: true,
|
||||||
|
}); // Increasing performance and giving some time for virtual list to settle down
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function is called by VirtualList for rendering each of the row
|
* A function is called by VirtualList for rendering each of the row
|
||||||
@ -188,7 +183,7 @@ export class LogList extends React.Component<Props> {
|
|||||||
*/
|
*/
|
||||||
getLogRow = (rowIndex: number) => {
|
getLogRow = (rowIndex: number) => {
|
||||||
const { searchQuery, isActiveOverlay } = searchStore;
|
const { searchQuery, isActiveOverlay } = searchStore;
|
||||||
const item = this.logs[rowIndex];
|
const item = this.props.logs[rowIndex];
|
||||||
const contents: React.ReactElement[] = [];
|
const contents: React.ReactElement[] = [];
|
||||||
const ansiToHtml = (ansi: string) => DOMPurify.sanitize(colorConverter.ansi_to_html(ansi));
|
const ansiToHtml = (ansi: string) => DOMPurify.sanitize(colorConverter.ansi_to_html(ansi));
|
||||||
|
|
||||||
@ -232,31 +227,26 @@ export class LogList extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isLoading } = this.props;
|
const { logs, isLoading, selectedContainer } = this.props;
|
||||||
const isInitLoading = isLoading && !this.logs.length;
|
|
||||||
const rowHeights = array.filled(this.logs.length, this.lineHeight);
|
|
||||||
|
|
||||||
if (isInitLoading) {
|
if (isLoading) {
|
||||||
return (
|
// Don't show a spinner since `Logs` will instead.
|
||||||
<div className="LogList flex box grow align-center justify-center">
|
return null;
|
||||||
<Spinner center/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.logs.length) {
|
if (!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 {selectedContainer}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("LogList flex", { isLoading })}>
|
<div className={cssNames("LogList flex")}>
|
||||||
<VirtualList
|
<VirtualList
|
||||||
items={this.logs}
|
items={logs}
|
||||||
rowHeights={rowHeights}
|
rowHeights={array.filled(logs.length, this.lineHeight)}
|
||||||
getRow={this.getLogRow}
|
getRow={this.getLogRow}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
outerRef={this.virtualListDiv}
|
outerRef={this.virtualListDiv}
|
||||||
|
|||||||
@ -21,6 +21,6 @@
|
|||||||
|
|
||||||
.LogResourceSelector {
|
.LogResourceSelector {
|
||||||
.Select {
|
.Select {
|
||||||
min-width: 150px;
|
width: 250px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,96 +21,104 @@
|
|||||||
|
|
||||||
import "./log-resource-selector.scss";
|
import "./log-resource-selector.scss";
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
import { Pod } from "../../../common/k8s-api/endpoints";
|
import type { Pod } from "../../../common/k8s-api/endpoints";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { Select, SelectOption } from "../select";
|
import { GroupSelectOption, Select, SelectOption } from "../select";
|
||||||
import { LogTabData, logTabStore } from "./log-tab.store";
|
import { LogTabData, logTabStore } from "./log-tab.store";
|
||||||
import { podsStore } from "../+workloads-pods/pods.store";
|
|
||||||
import type { TabId } from "./dock.store";
|
import type { TabId } from "./dock.store";
|
||||||
|
|
||||||
interface Props {
|
export interface LogResourceSelectorStore {
|
||||||
tabId: TabId
|
changeSelectedPod(tabId: TabId, newSelectedPod: Pod): void;
|
||||||
tabData: LogTabData
|
mergeData(tabId: TabId, data: Partial<LogTabData>): void;
|
||||||
save: (data: Partial<LogTabData>) => void
|
|
||||||
reload: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LogResourceSelector = observer((props: Props) => {
|
export interface LogResourceSelectorProps {
|
||||||
const { tabData, save, reload, tabId } = props;
|
tabId: TabId;
|
||||||
const { selectedPod, selectedContainer, pods } = tabData;
|
pod: Pod;
|
||||||
const pod = new Pod(selectedPod);
|
|
||||||
|
/**
|
||||||
|
* The list of possible pods to switch between. If not specifed then it won't be displayed
|
||||||
|
*/
|
||||||
|
pods?: Pod[];
|
||||||
|
selectedContainer: string;
|
||||||
|
store?: LogResourceSelectorStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogResourceSelector = observer(({ tabId, pod, pods, selectedContainer, store = logTabStore }: LogResourceSelectorProps) => {
|
||||||
const containers = pod.getContainers();
|
const containers = pod.getContainers();
|
||||||
const initContainers = pod.getInitContainers();
|
const initContainers = pod.getInitContainers();
|
||||||
|
|
||||||
const onContainerChange = (option: SelectOption) => {
|
const containerSelectOptions: GroupSelectOption<SelectOption<string>>[] = [
|
||||||
save({
|
|
||||||
selectedContainer: containers
|
|
||||||
.concat(initContainers)
|
|
||||||
.find(container => container.name === option.value),
|
|
||||||
});
|
|
||||||
reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPodChange = (option: SelectOption) => {
|
|
||||||
const selectedPod = podsStore.getByName(option.value, pod.getNs());
|
|
||||||
|
|
||||||
save({ selectedPod });
|
|
||||||
logTabStore.renameTab(tabId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSelectOptions = (items: string[]) => {
|
|
||||||
return items.map(item => {
|
|
||||||
return {
|
|
||||||
value: item,
|
|
||||||
label: item,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const containerSelectOptions = [
|
|
||||||
{
|
{
|
||||||
label: `Containers`,
|
label: `Containers`,
|
||||||
options: getSelectOptions(containers.map(container => container.name)),
|
options: containers.map(container => ({
|
||||||
|
value: container.name,
|
||||||
|
label: container.name,
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `Init Containers`,
|
label: `Init Containers`,
|
||||||
options: getSelectOptions(initContainers.map(container => container.name)),
|
options: initContainers.map(container => ({
|
||||||
|
value: container.name,
|
||||||
|
label: container.name,
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const podSelectOptions = [
|
const manyOptions = (containers.length + initContainers.length) > 1;
|
||||||
{
|
|
||||||
label: pod.getOwnerRefs()[0]?.name,
|
|
||||||
options: getSelectOptions(pods.map(pod => pod.metadata.name)),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload();
|
|
||||||
}, [selectedPod]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="LogResourceSelector flex gaps align-center">
|
<div className="LogResourceSelector flex gaps align-center">
|
||||||
<span>Namespace</span> <Badge data-testid="namespace-badge" label={pod.getNs()}/>
|
<span>Namespace</span>
|
||||||
<span>Pod</span>
|
<Badge data-testid="namespace-badge" label={pod.getNs()}/>
|
||||||
<Select
|
{
|
||||||
options={podSelectOptions}
|
pods && (
|
||||||
value={{ label: pod.getName(), value: pod.getName() }}
|
<>
|
||||||
onChange={onPodChange}
|
<span>Pod</span>
|
||||||
autoConvertOptions={false}
|
{
|
||||||
className="pod-selector"
|
pods.length === 1
|
||||||
/>
|
? (
|
||||||
|
<Badge data-testid="pod-badge" label={pod.getName()}/>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<Select
|
||||||
|
options={[{
|
||||||
|
label: pod.getOwnerRefs()[0].name,
|
||||||
|
options: pods.map(pod => ({
|
||||||
|
label: pod.getName(),
|
||||||
|
value: pod,
|
||||||
|
})),
|
||||||
|
}]}
|
||||||
|
value={{ label: pod.getName(), value: pod }}
|
||||||
|
onChange={({ value }) => store.changeSelectedPod(tabId, value)}
|
||||||
|
autoConvertOptions={false}
|
||||||
|
className="pod-selector"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<span>Container</span>
|
<span>Container</span>
|
||||||
<Select
|
{
|
||||||
options={containerSelectOptions}
|
manyOptions
|
||||||
value={{ label: selectedContainer.name, value: selectedContainer.name }}
|
? (
|
||||||
onChange={onContainerChange}
|
<Select
|
||||||
autoConvertOptions={false}
|
options={containerSelectOptions}
|
||||||
className="container-selector"
|
value={{ value: selectedContainer, label: selectedContainer }}
|
||||||
/>
|
onChange={({ value }) => store.mergeData(tabId, { selectedContainer: value })}
|
||||||
|
autoConvertOptions={false}
|
||||||
|
className="container-selector"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<Badge data-testid="container-badge" label={selectedContainer}/>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
.LogSearch {
|
.LogSearch {
|
||||||
.SearchInput {
|
.SearchInput {
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
|
width: 240px;
|
||||||
|
|
||||||
.find-count {
|
.find-count {
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
@ -31,4 +32,4 @@
|
|||||||
padding-bottom: 7px;
|
padding-bottom: 7px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,131 +19,172 @@
|
|||||||
* 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 Joi from "joi";
|
||||||
import uniqueId from "lodash/uniqueId";
|
import uniqueId from "lodash/uniqueId";
|
||||||
import { reaction } from "mobx";
|
import { action } from "mobx";
|
||||||
import { podsStore } from "../+workloads-pods/pods.store";
|
import type { IPodContainer, Pod } from "../../../common/k8s-api/endpoints";
|
||||||
|
import { DockTabStore, DockTabStoreOptions } from "./dock-tab.store";
|
||||||
import { IPodContainer, Pod } from "../../../common/k8s-api/endpoints";
|
import { dockStore, DockTab, DockTabCreate, TabId, TabKind } from "./dock.store";
|
||||||
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";
|
|
||||||
|
|
||||||
export interface LogTabData {
|
export interface LogTabData {
|
||||||
pods: Pod[];
|
/**
|
||||||
selectedPod: Pod;
|
* The pod owner ID.
|
||||||
selectedContainer: IPodContainer
|
*/
|
||||||
showTimestamps?: boolean
|
podsOwner?: string;
|
||||||
previous?: boolean
|
|
||||||
|
/**
|
||||||
|
* The ID of the pod from the list of pods owned by `.podsOwner`
|
||||||
|
*/
|
||||||
|
selectedPod: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The namespace of the pods so that the pods can be retrieved.
|
||||||
|
*/
|
||||||
|
namespace: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the container within the selected pod.
|
||||||
|
*
|
||||||
|
* Note: container names are guaranteed unique
|
||||||
|
*/
|
||||||
|
selectedContainer?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to show timestamps inline with the logs
|
||||||
|
*/
|
||||||
|
showTimestamps: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query for getting logs of the previous container restart
|
||||||
|
*/
|
||||||
|
previous: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PodLogsTabData {
|
const logTabDataValidator = Joi.object({
|
||||||
|
podsOwner: Joi
|
||||||
|
.string()
|
||||||
|
.optional(),
|
||||||
|
selectedPod: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
namespace: Joi
|
||||||
|
.string()
|
||||||
|
.required(),
|
||||||
|
selectedContainer: Joi
|
||||||
|
.string()
|
||||||
|
.optional(),
|
||||||
|
showTimestamps: Joi
|
||||||
|
.boolean()
|
||||||
|
.required(),
|
||||||
|
previous: Joi
|
||||||
|
.boolean()
|
||||||
|
.required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data for creating a pod logs tab based on a specific pod
|
||||||
|
*/
|
||||||
|
export interface PodLogsTabData {
|
||||||
selectedPod: Pod
|
selectedPod: Pod
|
||||||
selectedContainer: IPodContainer
|
selectedContainer: IPodContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkloadLogsTabData {
|
export interface DockManager {
|
||||||
workload: WorkloadKubeObject
|
renameTab(tabId: TabId, name: string): void;
|
||||||
|
createTab(rawTabDesc: DockTabCreate, addNumber?: boolean): DockTab;
|
||||||
|
closeTab(tabId: TabId): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LogTabStore extends DockTabStore<LogTabData> {
|
export class LogTabStore extends DockTabStore<LogTabData> {
|
||||||
constructor() {
|
constructor(params: Pick<DockTabStoreOptions<LogTabData>, "autoInit"> = {}, protected dockManager: DockManager = dockStore) {
|
||||||
super({
|
super({
|
||||||
|
...params,
|
||||||
storageKey: "pod_logs",
|
storageKey: "pod_logs",
|
||||||
});
|
validator: value => {
|
||||||
|
const { error } = logTabDataValidator.validate(value);
|
||||||
|
|
||||||
reaction(() => podsStore.items.length, () => this.updateTabsData());
|
if (error) {
|
||||||
}
|
throw error;
|
||||||
|
}
|
||||||
createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): string {
|
},
|
||||||
const podOwner = selectedPod.getOwnerRefs()[0];
|
|
||||||
const pods = podsStore.getPodsByOwnerId(podOwner?.uid);
|
|
||||||
const title = `Pod ${selectedPod.getName()}`;
|
|
||||||
|
|
||||||
return this.createLogsTab(title, {
|
|
||||||
pods: pods.length ? pods : [selectedPod],
|
|
||||||
selectedPod,
|
|
||||||
selectedContainer,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createWorkloadTab({ workload }: WorkloadLogsTabData): void {
|
createPodTab(tabData: PodLogsTabData): string {
|
||||||
const pods = podsStore.getPodsByOwnerId(workload.getId());
|
if (!tabData || typeof tabData !== "object") {
|
||||||
|
throw new TypeError("tabData is not an object");
|
||||||
|
}
|
||||||
|
|
||||||
if (!pods.length) return;
|
const { selectedPod, selectedContainer } = tabData;
|
||||||
|
|
||||||
const selectedPod = pods[0];
|
if (!selectedPod || typeof selectedPod !== "object") {
|
||||||
const selectedContainer = selectedPod.getAllContainers()[0];
|
throw new TypeError("selectedPod is not an object");
|
||||||
const title = `${workload.kind} ${selectedPod.getName()}`;
|
}
|
||||||
|
|
||||||
this.createLogsTab(title, {
|
if (!selectedContainer || typeof selectedContainer !== "object") {
|
||||||
pods,
|
throw new TypeError("selectedContainer is not an object");
|
||||||
selectedPod,
|
}
|
||||||
selectedContainer,
|
|
||||||
|
return this.createLogsTab(this.getTabName(selectedPod), {
|
||||||
|
podsOwner: selectedPod.getOwnerRefs()[0]?.uid,
|
||||||
|
namespace: selectedPod.getNs(),
|
||||||
|
selectedPod: selectedPod.getId(),
|
||||||
|
selectedContainer: selectedContainer.name,
|
||||||
|
showTimestamps: false,
|
||||||
|
previous: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renameTab(tabId: string) {
|
private getTabName(pod: Pod): string {
|
||||||
const { selectedPod } = this.getData(tabId);
|
return `Pod Logs: ${pod.getName()}`;
|
||||||
|
|
||||||
dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createDockTab(tabParams: DockTabCreateSpecific) {
|
@action
|
||||||
dockStore.createTab({
|
changeSelectedPod(tabId: string, pod: Pod): void {
|
||||||
...tabParams,
|
const oldSelectedPod = this.getData(tabId).selectedPod;
|
||||||
kind: TabKind.POD_LOGS,
|
|
||||||
}, false);
|
if (pod.getId() === oldSelectedPod) {
|
||||||
}
|
// Do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mergeData(tabId, {
|
||||||
|
selectedPod: pod.getId(),
|
||||||
|
selectedContainer: pod.getContainers()[0]?.name,
|
||||||
|
});
|
||||||
|
this.dockManager.renameTab(tabId, this.getTabName(pod));
|
||||||
|
}
|
||||||
|
|
||||||
private createLogsTab(title: string, data: LogTabData): string {
|
private createLogsTab(title: string, data: LogTabData): string {
|
||||||
const id = uniqueId("log-tab-");
|
const id = uniqueId("log-tab-");
|
||||||
|
|
||||||
this.createDockTab({ id, title });
|
this.dockManager.createTab({
|
||||||
this.setData(id, {
|
id,
|
||||||
...data,
|
title,
|
||||||
showTimestamps: false,
|
kind: TabKind.POD_LOGS,
|
||||||
previous: false,
|
}, false);
|
||||||
});
|
this.setData(id, data);
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTabsData() {
|
@action
|
||||||
for (const [tabId, tabData] of this.data) {
|
public closeTab(tabId: string) {
|
||||||
try {
|
this.clearData(tabId);
|
||||||
if (!tabData.selectedPod) {
|
this.dockManager.closeTab(tabId);
|
||||||
tabData.selectedPod = tabData.pods[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const pod = new Pod(tabData.selectedPod);
|
|
||||||
const pods = podsStore.getPodsByOwnerId(pod.getOwnerRefs()[0]?.uid);
|
|
||||||
const isSelectedPodInList = pods.find(item => item.getId() == pod.getId());
|
|
||||||
const selectedPod = isSelectedPodInList ? pod : pods[0];
|
|
||||||
const selectedContainer = isSelectedPodInList ? tabData.selectedContainer : pod.getAllContainers()[0];
|
|
||||||
|
|
||||||
if (pods.length > 0) {
|
|
||||||
this.setData(tabId, {
|
|
||||||
...tabData,
|
|
||||||
selectedPod,
|
|
||||||
selectedContainer,
|
|
||||||
pods,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.renameTab(tabId);
|
|
||||||
} else {
|
|
||||||
this.closeTab(tabId);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[LOG-TAB-STORE]: failed to set data for tabId=${tabId} deleting`, error);
|
|
||||||
this.data.delete(tabId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private closeTab(tabId: string) {
|
/**
|
||||||
this.clearData(tabId);
|
* Get a set of namespaces which pod tabs care about
|
||||||
dockStore.closeTab(tabId);
|
*/
|
||||||
|
public getNamespaces(): string[] {
|
||||||
|
const namespaces = new Set<string>();
|
||||||
|
|
||||||
|
for (const { namespace } of this.data.values()) {
|
||||||
|
namespaces.add(namespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...namespaces];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,40 +19,42 @@
|
|||||||
* 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 { autorun, computed, observable, makeObservable } from "mobx";
|
import { observable, makeObservable } from "mobx";
|
||||||
|
import moment from "moment";
|
||||||
|
import { podsStore } from "../+workloads-pods/pods.store";
|
||||||
|
|
||||||
import { IPodLogsQuery, Pod, podsApi } from "../../../common/k8s-api/endpoints";
|
import { IPodLogsQuery, podsApi } from "../../../common/k8s-api/endpoints";
|
||||||
import { autoBind, interval } from "../../utils";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { dockStore, TabId, TabKind } from "./dock.store";
|
import { autoBind } from "../../utils";
|
||||||
import { logTabStore } from "./log-tab.store";
|
import type { TabId } from "./dock.store";
|
||||||
|
import type { LogTabData } from "./log-tab.store";
|
||||||
|
|
||||||
type PodLogLine = string;
|
type PodLogLine = string;
|
||||||
|
|
||||||
const logLinesToLoad = 500;
|
const logLinesToLoad = 500;
|
||||||
|
|
||||||
|
function removeTimestamp(logLine: string) {
|
||||||
|
return logLine.replace(/^\d+.*?\s/gm, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(log: string, tz: string): string {
|
||||||
|
const extraction = /^(?<timestamp>\d+\S+)(?<line>.*)/.exec(log);
|
||||||
|
|
||||||
|
if (!extraction) {
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { timestamp, line } = extraction.groups;
|
||||||
|
|
||||||
|
return `${moment.tz(timestamp, tz).format()}${line}`;
|
||||||
|
}
|
||||||
|
|
||||||
export class LogStore {
|
export class LogStore {
|
||||||
private refresher = interval(10, () => {
|
|
||||||
const id = dockStore.selectedTabId;
|
|
||||||
|
|
||||||
if (!this.podLogs.get(id)) return;
|
|
||||||
this.loadMore(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
@observable podLogs = observable.map<TabId, PodLogLine[]>();
|
@observable podLogs = observable.map<TabId, PodLogLine[]>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
|
|
||||||
autorun(() => {
|
|
||||||
const { selectedTab, isOpen } = dockStore;
|
|
||||||
|
|
||||||
if (selectedTab?.kind === TabKind.POD_LOGS && isOpen) {
|
|
||||||
this.refresher.start();
|
|
||||||
} else {
|
|
||||||
this.refresher.stop();
|
|
||||||
}
|
|
||||||
}, { delay: 500 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handlerError(tabId: TabId, error: any): void {
|
handlerError(tabId: TabId, error: any): void {
|
||||||
@ -65,7 +67,6 @@ export class LogStore {
|
|||||||
`Reason: ${error.reason} (${error.code})`,
|
`Reason: ${error.reason} (${error.code})`,
|
||||||
];
|
];
|
||||||
|
|
||||||
this.refresher.stop();
|
|
||||||
this.podLogs.set(tabId, message);
|
this.podLogs.set(tabId, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,13 +77,14 @@ export class LogStore {
|
|||||||
* messages
|
* messages
|
||||||
* @param tabId
|
* @param tabId
|
||||||
*/
|
*/
|
||||||
load = async (tabId: TabId) => {
|
load = async (tabId: TabId, data: LogTabData) => {
|
||||||
try {
|
try {
|
||||||
const logs = await this.loadLogs(tabId, {
|
const prevLogsLength = this.podLogs.get(tabId)?.length ?? 0;
|
||||||
tailLines: this.lines + logLinesToLoad,
|
const params = {
|
||||||
});
|
tailLines: prevLogsLength + logLinesToLoad,
|
||||||
|
};
|
||||||
|
const logs = await this.loadLogs(data, params);
|
||||||
|
|
||||||
this.refresher.start();
|
|
||||||
this.podLogs.set(tabId, logs);
|
this.podLogs.set(tabId, logs);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handlerError(tabId, error);
|
this.handlerError(tabId, error);
|
||||||
@ -95,19 +97,18 @@ export class LogStore {
|
|||||||
* starting from last line received.
|
* starting from last line received.
|
||||||
* @param tabId
|
* @param tabId
|
||||||
*/
|
*/
|
||||||
loadMore = async (tabId: TabId) => {
|
loadMore = async (tabId: TabId, data: LogTabData) => {
|
||||||
if (!this.podLogs.get(tabId).length) {
|
if (!this.podLogs.get(tabId).length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const oldLogs = this.podLogs.get(tabId);
|
const logs = await this.loadLogs(data, {
|
||||||
const logs = await this.loadLogs(tabId, {
|
|
||||||
sinceTime: this.getLastSinceTime(tabId),
|
sinceTime: this.getLastSinceTime(tabId),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add newly received logs to bottom
|
// Add newly received logs to bottom
|
||||||
this.podLogs.set(tabId, [...oldLogs, ...logs.filter(Boolean)]);
|
this.podLogs.get(tabId).push(...logs.filter(Boolean));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handlerError(tabId, error);
|
this.handlerError(tabId, error);
|
||||||
}
|
}
|
||||||
@ -120,48 +121,35 @@ 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
|
||||||
*/
|
*/
|
||||||
async loadLogs(tabId: TabId, params: Partial<IPodLogsQuery>): Promise<string[]> {
|
async loadLogs(data: LogTabData, params: Partial<IPodLogsQuery>): Promise<string[]> {
|
||||||
const data = logTabStore.getData(tabId);
|
const { podsOwner, selectedContainer, selectedPod, previous } = data;
|
||||||
const { selectedContainer, previous } = data;
|
const pod = podsOwner
|
||||||
const pod = new Pod(data.selectedPod);
|
? podsStore.getPodsByOwnerId(podsOwner).find(pod => pod.getId() === selectedPod)
|
||||||
|
: podsStore.getById(selectedPod);
|
||||||
|
|
||||||
|
if (!pod) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
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 podsApi.getLogs({ 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,
|
||||||
previous,
|
previous,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.trimEnd().split("\n");
|
return result.split("\n").filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getLogs(tabId: TabId, { showTimestamps }: LogTabData): string[] {
|
||||||
* Converts logs into a string array
|
const logs = this.podLogs.get(tabId) ?? [];
|
||||||
* @returns Length of log lines
|
const { localeTimezone } = UserStore.getInstance();
|
||||||
*/
|
|
||||||
@computed
|
|
||||||
get lines(): number {
|
|
||||||
return this.logs.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return showTimestamps
|
||||||
/**
|
? logs.map(log => formatTimestamp(log, localeTimezone))
|
||||||
* Returns logs with timestamps for selected tab
|
: logs.map(removeTimestamp);
|
||||||
*/
|
|
||||||
@computed
|
|
||||||
get logs() {
|
|
||||||
return this.podLogs.get(dockStore.selectedTabId) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes timestamps from each log line and returns changed logs
|
|
||||||
* @returns Logs without timestamps
|
|
||||||
*/
|
|
||||||
@computed
|
|
||||||
get logsWithoutTimestamps() {
|
|
||||||
return this.logs.map(item => this.removeTimestamps(item));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -179,24 +167,33 @@ export class LogStore {
|
|||||||
return stamp.toISOString();
|
return stamp.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
splitOutTimestamp(logs: string): [string, string] {
|
/**
|
||||||
const extraction = /^(\d+\S+)(.*)/m.exec(logs);
|
* Get the local formatted date of the first log line's timestamp
|
||||||
|
* @param tabId The ID of the tab to get the time of
|
||||||
|
* @returns `""` if no logs, or log does not have a timestamp
|
||||||
|
*/
|
||||||
|
getFirstTime(tabId: TabId): string {
|
||||||
|
const logs = this.podLogs.get(tabId);
|
||||||
|
|
||||||
if (!extraction || extraction.length < 3) {
|
if (!logs?.length) {
|
||||||
return ["", logs];
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return [extraction[1], extraction[2]];
|
const timestamps = this.getTimestamps(logs[0]);
|
||||||
|
|
||||||
|
if (!timestamps) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const stamp = new Date(timestamps[0]);
|
||||||
|
|
||||||
|
return stamp.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
getTimestamps(logs: string) {
|
getTimestamps(logs: string) {
|
||||||
return logs.match(/^\d+\S+/gm);
|
return logs.match(/^\d+\S+/gm);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTimestamps(logs: string) {
|
|
||||||
return logs.replace(/^\d+.*?\s/gm, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
clearLogs(tabId: TabId) {
|
clearLogs(tabId: TabId) {
|
||||||
this.podLogs.delete(tabId);
|
this.podLogs.delete(tabId);
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/renderer/components/dock/logs.scss
Normal file
27
src/renderer/components/dock/logs.scss
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.PodLogs {
|
||||||
|
.pod-not-found {
|
||||||
|
margin: auto;
|
||||||
|
color: var(--terminalBrightWhite);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,12 +19,13 @@
|
|||||||
* 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 "./logs.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observable, reaction, makeObservable } from "mobx";
|
import { observable, makeObservable, computed, when, reaction, runInAction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
|
||||||
import { searchStore } from "../../../common/search-store";
|
import { searchStore } from "../../../common/search-store";
|
||||||
import { boundMethod } from "../../utils";
|
|
||||||
import type { DockTab } from "./dock.store";
|
import type { DockTab } from "./dock.store";
|
||||||
import { InfoPanel } from "./info-panel";
|
import { InfoPanel } from "./info-panel";
|
||||||
import { LogResourceSelector } from "./log-resource-selector";
|
import { LogResourceSelector } from "./log-resource-selector";
|
||||||
@ -33,15 +34,31 @@ import { logStore } from "./log.store";
|
|||||||
import { LogSearch } from "./log-search";
|
import { LogSearch } from "./log-search";
|
||||||
import { LogControls } from "./log-controls";
|
import { LogControls } from "./log-controls";
|
||||||
import { LogTabData, logTabStore } from "./log-tab.store";
|
import { LogTabData, logTabStore } from "./log-tab.store";
|
||||||
|
import { podsStore } from "../+workloads-pods/pods.store";
|
||||||
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
import { Spinner } from "../spinner";
|
||||||
|
import { disposingReaction, comparer } from "../../utils";
|
||||||
|
import type { Pod } from "../../../common/k8s-api/endpoints";
|
||||||
|
import { Badge } from "../badge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
tab: DockTab
|
tab: DockTab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GottenPods {
|
||||||
|
pods?: Pod[];
|
||||||
|
pod: Pod | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Logs extends React.Component<Props> {
|
export class Logs extends React.Component<Props> {
|
||||||
@observable isLoading = true;
|
@observable isLoading = false;
|
||||||
|
/**
|
||||||
|
* Only used for the inital loading of logs so that when logs are shorter
|
||||||
|
* than the viewport the user doesn't get incessant spinner every 700ms
|
||||||
|
*/
|
||||||
|
@observable isLoadingInitial = true;
|
||||||
|
|
||||||
private logListElement = React.createRef<LogList>(); // A reference for VirtualList component
|
private logListElement = React.createRef<LogList>(); // A reference for VirtualList component
|
||||||
|
|
||||||
@ -51,40 +68,125 @@ export class Logs extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this,
|
disposeOnUnmount(this, [
|
||||||
reaction(() => this.props.tab.id, this.reload, { fireImmediately: true }),
|
kubeWatchApi.subscribeStores([
|
||||||
|
podsStore,
|
||||||
|
], {
|
||||||
|
namespaces: logTabStore.getNamespaces(),
|
||||||
|
}),
|
||||||
|
when(() => this.canLoad, () => this.load(true)),
|
||||||
|
this.loadWhenPossibleOnTabChange(),
|
||||||
|
this.changeSelectedPodWhenCurrentDisappears(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only used in `componentDidMount`
|
||||||
|
*/
|
||||||
|
private loadWhenPossibleOnTabChange() {
|
||||||
|
return disposingReaction(
|
||||||
|
() => [this.tabId, this.tabData] as const,
|
||||||
|
([curTabId], [oldTabId]) => {
|
||||||
|
if (curTabId !== oldTabId) {
|
||||||
|
logStore.clearLogs(this.tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return when(() => this.canLoad, () => this.load(true));
|
||||||
|
},
|
||||||
|
{
|
||||||
|
equals: comparer.structural,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get tabId() {
|
/**
|
||||||
|
* Only used in `componentDidMount`
|
||||||
|
*/
|
||||||
|
private changeSelectedPodWhenCurrentDisappears() {
|
||||||
|
return reaction(() => this.getPods(this.tabData), (data) => {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pods, pod } = data;
|
||||||
|
|
||||||
|
if (pods && !pod && pods.length > 0) {
|
||||||
|
const selectedPod = pods[0];
|
||||||
|
|
||||||
|
logTabStore.mergeData(this.tabId, {
|
||||||
|
selectedPod: selectedPod.getId(),
|
||||||
|
selectedContainer: selectedPod.getContainers()[0]?.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
fireImmediately: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPods(data: LogTabData): GottenPods;
|
||||||
|
private getPods(data: LogTabData | undefined): GottenPods | undefined {
|
||||||
|
if (!data) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { podsOwner, selectedPod } = data;
|
||||||
|
|
||||||
|
if (podsOwner) {
|
||||||
|
const pods = podsStore.getPodsByOwnerId(podsOwner);
|
||||||
|
const pod = pods.find(pod => pod.getId() === selectedPod);
|
||||||
|
|
||||||
|
return { pods, pod };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { pod: podsStore.getById(selectedPod) };
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get tabData(): LogTabData {
|
||||||
|
return logTabStore.getData(this.tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get tabId() {
|
||||||
return this.props.tab.id;
|
return this.props.tab.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
load = async () => {
|
@computed get canSwap(): boolean {
|
||||||
this.isLoading = true;
|
const data = this.tabData;
|
||||||
await logStore.load(this.tabId);
|
|
||||||
this.isLoading = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
reload = async () => {
|
if (!data) {
|
||||||
logStore.clearLogs(this.tabId);
|
return false;
|
||||||
await this.load();
|
}
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
const { podsOwner } = data;
|
||||||
* A function for various actions after search is happened
|
|
||||||
* @param query {string} A text from search field
|
if (!podsOwner) {
|
||||||
*/
|
return false;
|
||||||
@boundMethod
|
}
|
||||||
onSearch() {
|
|
||||||
this.toOverlay();
|
return podsStore.getPodsByOwnerId(podsOwner).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed get canLoad(): boolean {
|
||||||
|
return Boolean(this.getPods(this.tabData)?.pod);
|
||||||
|
}
|
||||||
|
|
||||||
|
load = async (initial = false) => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.isLoadingInitial = initial;
|
||||||
|
});
|
||||||
|
|
||||||
|
await logStore.load(this.tabId, this.tabData);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.isLoadingInitial = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scrolling to active overlay (search word highlight)
|
* Scrolling to active overlay (search word highlight)
|
||||||
*/
|
*/
|
||||||
@boundMethod
|
onSearch = () => {
|
||||||
toOverlay() {
|
|
||||||
const { activeOverlayLine } = searchStore;
|
const { activeOverlayLine } = searchStore;
|
||||||
|
|
||||||
if (!this.logListElement.current || activeOverlayLine === undefined) return;
|
if (!this.logListElement.current || activeOverlayLine === undefined) return;
|
||||||
@ -92,71 +194,86 @@ export class Logs extends React.Component<Props> {
|
|||||||
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(() => {
|
||||||
const overlay = document.querySelector(".PodLogs .list span.active");
|
document.querySelector(".PodLogs .list span.active")?.scrollIntoViewIfNeeded();
|
||||||
|
|
||||||
if (!overlay) return;
|
|
||||||
overlay.scrollIntoViewIfNeeded();
|
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const data = this.tabData;
|
||||||
|
|
||||||
renderResourceSelector(data?: LogTabData) {
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logs = logStore.logs;
|
if (!podsStore.isLoaded) {
|
||||||
const searchLogs = data.showTimestamps ? logs : logStore.logsWithoutTimestamps;
|
return <Spinner center />;
|
||||||
const controls = (
|
}
|
||||||
<div className="flex gaps">
|
|
||||||
<LogResourceSelector
|
|
||||||
tabId={this.tabId}
|
|
||||||
tabData={data}
|
|
||||||
save={newData => logTabStore.setData(this.tabId, { ...data, ...newData })}
|
|
||||||
reload={this.reload}
|
|
||||||
/>
|
|
||||||
<LogSearch
|
|
||||||
onSearch={this.onSearch}
|
|
||||||
logs={searchLogs}
|
|
||||||
toPrevOverlay={this.toOverlay}
|
|
||||||
toNextOverlay={this.toOverlay}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
const logs = logStore.getLogs(this.tabId, data);
|
||||||
<InfoPanel
|
const { podsOwner, selectedContainer, selectedPod, showTimestamps, previous, namespace } = data;
|
||||||
tabId={this.props.tab.id}
|
const { pods, pod } = this.getPods(data);
|
||||||
controls={controls}
|
|
||||||
showSubmitClose={false}
|
|
||||||
showButtons={false}
|
|
||||||
showStatusPanel={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
if (!pod) {
|
||||||
const logs = logStore.logs;
|
return (
|
||||||
const data = logTabStore.getData(this.tabId);
|
<div className="PodLogs flex column">
|
||||||
|
<InfoPanel
|
||||||
if (!data) {
|
tabId={this.props.tab.id}
|
||||||
this.reload();
|
controls={
|
||||||
|
<div className="flex gaps align-center">
|
||||||
|
<span>Namespace</span>
|
||||||
|
<Badge label={namespace} />
|
||||||
|
<span>Pod</span>
|
||||||
|
<Badge label={selectedPod || "???"} />
|
||||||
|
<span>Container</span>
|
||||||
|
<Badge label={selectedContainer || "???"} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
showSubmitClose={false}
|
||||||
|
showButtons={false}
|
||||||
|
showStatusPanel={false}
|
||||||
|
/>
|
||||||
|
<p className="pod-not-found">Pod is no longer found {podsOwner && `under owner ${podsOwner}`}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="PodLogs flex column">
|
<div className="PodLogs flex column">
|
||||||
{this.renderResourceSelector(data)}
|
<InfoPanel
|
||||||
|
tabId={this.props.tab.id}
|
||||||
|
controls={
|
||||||
|
<div className="flex gaps">
|
||||||
|
<LogResourceSelector
|
||||||
|
tabId={this.tabId}
|
||||||
|
pod={pod}
|
||||||
|
pods={pods}
|
||||||
|
selectedContainer={selectedContainer}
|
||||||
|
/>
|
||||||
|
{this.isLoading && <Spinner />}
|
||||||
|
<LogSearch
|
||||||
|
onSearch={this.onSearch}
|
||||||
|
logs={logs}
|
||||||
|
toPrevOverlay={this.onSearch}
|
||||||
|
toNextOverlay={this.onSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
showSubmitClose={false}
|
||||||
|
showButtons={false}
|
||||||
|
showStatusPanel={false}
|
||||||
|
/>
|
||||||
<LogList
|
<LogList
|
||||||
logs={logs}
|
logs={logs}
|
||||||
id={this.tabId}
|
selectedContainer={selectedContainer}
|
||||||
isLoading={this.isLoading}
|
isLoading={this.isLoadingInitial}
|
||||||
load={this.load}
|
load={this.load}
|
||||||
ref={this.logListElement}
|
ref={this.logListElement}
|
||||||
/>
|
/>
|
||||||
<LogControls
|
<LogControls
|
||||||
|
tabId={this.tabId}
|
||||||
|
pod={pod}
|
||||||
|
preferences={{ previous, showTimestamps }}
|
||||||
logs={logs}
|
logs={logs}
|
||||||
tabData={data}
|
|
||||||
save={newData => logTabStore.setData(this.tabId, { ...data, ...newData })}
|
|
||||||
reload={this.reload}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import fse from "fs-extra";
|
|||||||
import { StorageHelper } from "./storageHelper";
|
import { StorageHelper } from "./storageHelper";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
import { isTestEnv } from "../../common/vars";
|
import { isTestEnv } from "../../common/vars";
|
||||||
|
import AwaitLock from "await-lock";
|
||||||
|
|
||||||
const storage = observable({
|
const storage = observable({
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@ -50,8 +51,11 @@ export function createStorage<T>(key: string, defaultValue: T) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
storage.data = await fse.readJson(filePath);
|
storage.data = await fse.readJson(filePath);
|
||||||
} catch {
|
} catch (error) {
|
||||||
// ignore error
|
// ignore file not found errors for logging
|
||||||
|
if (error?.code !== "ENOENT") {
|
||||||
|
logger.warn(`${logPrefix} failed to read JSON from ${filePath}`, error);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!isTestEnv) {
|
if (!isTestEnv) {
|
||||||
logger.info(`${logPrefix} loading finished for ${filePath}`);
|
logger.info(`${logPrefix} loading finished for ${filePath}`);
|
||||||
@ -60,22 +64,29 @@ export function createStorage<T>(key: string, defaultValue: T) {
|
|||||||
storage.loaded = true;
|
storage.loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lock = new AwaitLock();
|
||||||
|
|
||||||
// bind auto-saving data changes to %storage-file.json
|
// bind auto-saving data changes to %storage-file.json
|
||||||
reaction(() => toJS(storage.data), saveFile, {
|
reaction(() => toJS(storage.data), saveFile, {
|
||||||
delay: 250, // lazy, avoid excessive writes to fs
|
delay: 1000, // lazy, avoid excessive writes to fs
|
||||||
equals: comparer.structural, // save only when something really changed
|
equals: comparer.structural, // save only when something really changed
|
||||||
});
|
});
|
||||||
|
|
||||||
async function saveFile(state: Record<string, any> = {}) {
|
async function saveFile(state: Record<string, any> = {}) {
|
||||||
logger.info(`${logPrefix} saving ${filePath}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await lock.acquireAsync();
|
||||||
|
logger.info(`${logPrefix} saving ${filePath}`);
|
||||||
await fse.ensureDir(path.dirname(filePath), { mode: 0o755 });
|
await fse.ensureDir(path.dirname(filePath), { mode: 0o755 });
|
||||||
await fse.writeJson(filePath, state, { spaces: 2 });
|
await fse.writeJson(filePath, state, { spaces: 2 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`${logPrefix} saving failed: ${error}`, {
|
const meta = {
|
||||||
json: state, jsonFilePath: filePath,
|
json: state,
|
||||||
});
|
jsonFilePath: filePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.error(`${logPrefix} saving failed: ${error}`, meta);
|
||||||
|
} finally {
|
||||||
|
lock.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user