mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix pod logs dock tab (#4738)
- Move all dependencies into a transient LogsViewModel - Remove dependencies on dockStore.selectedTab - Make all bindings as late as possible, as per mobx rules Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
b037c3bf02
commit
b3df5b4fc6
@ -25,3 +25,14 @@ export function getOrInsert<K, V>(map: Map<K, V>, key: K, value: V): V {
|
|||||||
export function getOrInsertMap<K, MK, MV>(map: Map<K, Map<MK, MV>>, key: K): Map<MK, MV> {
|
export function getOrInsertMap<K, MK, MV>(map: Map<K, Map<MK, MV>>, key: K): Map<MK, MV> {
|
||||||
return getOrInsert(map, key, new Map<MK, MV>());
|
return getOrInsert(map, key, new Map<MK, MV>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `getOrInsert` but with delayed creation of the item
|
||||||
|
*/
|
||||||
|
export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, value: () => V): V {
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, value());
|
||||||
|
}
|
||||||
|
|
||||||
|
return map.get(key);
|
||||||
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-
|
|||||||
import createTerminalTabInjectable from "../../renderer/components/dock/create-terminal-tab/create-terminal-tab.injectable";
|
import createTerminalTabInjectable from "../../renderer/components/dock/create-terminal-tab/create-terminal-tab.injectable";
|
||||||
import terminalStoreInjectable from "../../renderer/components/dock/terminal-store/terminal-store.injectable";
|
import terminalStoreInjectable from "../../renderer/components/dock/terminal-store/terminal-store.injectable";
|
||||||
import { asLegacyGlobalObjectForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
import { asLegacyGlobalObjectForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||||
import logTabStoreInjectable from "../../renderer/components/dock/log-tab-store/log-tab-store.injectable";
|
import logTabStoreInjectable from "../../renderer/components/dock/logs/tab-store.injectable";
|
||||||
import { asLegacyGlobalSingletonForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-singleton-for-extension-api";
|
import { asLegacyGlobalSingletonForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-singleton-for-extension-api";
|
||||||
import { TerminalStore as TerminalStoreClass } from "../../renderer/components/dock/terminal-store/terminal.store";
|
import { TerminalStore as TerminalStoreClass } from "../../renderer/components/dock/terminal-store/terminal.store";
|
||||||
|
|
||||||
|
|||||||
@ -1,151 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
|
||||||
import * as selectEvent from "react-select-event";
|
|
||||||
import { Pod } from "../../../../common/k8s-api/endpoints";
|
|
||||||
import { LogResourceSelector } from "../log-resource-selector";
|
|
||||||
import type { LogTabData } from "../log-tab-store/log-tab.store";
|
|
||||||
import { dockerPod, deploymentPod1 } from "./pod.mock";
|
|
||||||
import { ThemeStore } from "../../../theme.store";
|
|
||||||
import { UserStore } from "../../../../common/user-store";
|
|
||||||
import mockFs from "mock-fs";
|
|
||||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
|
||||||
import type { DiRender } from "../../test-utils/renderFor";
|
|
||||||
import { renderFor } from "../../test-utils/renderFor";
|
|
||||||
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
|
||||||
import callForLogsInjectable from "../log-store/call-for-logs/call-for-logs.injectable";
|
|
||||||
|
|
||||||
jest.mock("electron", () => ({
|
|
||||||
app: {
|
|
||||||
getVersion: () => "99.99.99",
|
|
||||||
getName: () => "lens",
|
|
||||||
setName: jest.fn(),
|
|
||||||
setPath: jest.fn(),
|
|
||||||
getPath: () => "tmp",
|
|
||||||
getLocale: () => "en",
|
|
||||||
setLoginItemSettings: jest.fn(),
|
|
||||||
},
|
|
||||||
ipcMain: {
|
|
||||||
on: jest.fn(),
|
|
||||||
handle: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const getComponent = (tabData: LogTabData) => {
|
|
||||||
return (
|
|
||||||
<LogResourceSelector
|
|
||||||
tabId="tabId"
|
|
||||||
tabData={tabData}
|
|
||||||
save={jest.fn()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getOnePodTabData = (): LogTabData => {
|
|
||||||
const selectedPod = new Pod(dockerPod);
|
|
||||||
|
|
||||||
return {
|
|
||||||
pods: [] as Pod[],
|
|
||||||
selectedPod,
|
|
||||||
selectedContainer: selectedPod.getContainers()[0],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFewPodsTabData = (): LogTabData => {
|
|
||||||
const selectedPod = new Pod(deploymentPod1);
|
|
||||||
const anotherPod = new Pod(dockerPod);
|
|
||||||
|
|
||||||
return {
|
|
||||||
pods: [anotherPod],
|
|
||||||
selectedPod,
|
|
||||||
selectedContainer: selectedPod.getContainers()[0],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("<LogResourceSelector />", () => {
|
|
||||||
let render: DiRender;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
|
||||||
|
|
||||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
|
||||||
di.override(callForLogsInjectable, () => () => Promise.resolve("some-logs"));
|
|
||||||
|
|
||||||
render = renderFor(di);
|
|
||||||
|
|
||||||
await di.runSetups();
|
|
||||||
|
|
||||||
mockFs({
|
|
||||||
"tmp": {},
|
|
||||||
});
|
|
||||||
|
|
||||||
UserStore.createInstance();
|
|
||||||
ThemeStore.createInstance();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
UserStore.resetInstance();
|
|
||||||
ThemeStore.resetInstance();
|
|
||||||
mockFs.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders w/o errors", () => {
|
|
||||||
const tabData = getOnePodTabData();
|
|
||||||
const { container } = render(getComponent(tabData));
|
|
||||||
|
|
||||||
expect(container).toBeInstanceOf(HTMLElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders proper namespace", () => {
|
|
||||||
const tabData = getOnePodTabData();
|
|
||||||
const { getByTestId } = render(getComponent(tabData));
|
|
||||||
const ns = getByTestId("namespace-badge");
|
|
||||||
|
|
||||||
expect(ns).toHaveTextContent("default");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders proper selected items within dropdowns", () => {
|
|
||||||
const tabData = getOnePodTabData();
|
|
||||||
const { getByText } = render(getComponent(tabData));
|
|
||||||
|
|
||||||
expect(getByText("dockerExporter")).toBeInTheDocument();
|
|
||||||
expect(getByText("docker-exporter")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders sibling pods in dropdown", () => {
|
|
||||||
const tabData = getFewPodsTabData();
|
|
||||||
const { container, getByText } = render(getComponent(tabData));
|
|
||||||
const podSelector: HTMLElement = container.querySelector(".pod-selector");
|
|
||||||
|
|
||||||
selectEvent.openMenu(podSelector);
|
|
||||||
|
|
||||||
expect(getByText("dockerExporter")).toBeInTheDocument();
|
|
||||||
expect(getByText("deploymentPod1")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders sibling containers in dropdown", () => {
|
|
||||||
const tabData = getFewPodsTabData();
|
|
||||||
const { getByText, container } = render(getComponent(tabData));
|
|
||||||
const containerSelector: HTMLElement = container.querySelector(".container-selector");
|
|
||||||
|
|
||||||
selectEvent.openMenu(containerSelector);
|
|
||||||
|
|
||||||
expect(getByText("node-exporter-1")).toBeInTheDocument();
|
|
||||||
expect(getByText("init-node-exporter")).toBeInTheDocument();
|
|
||||||
expect(getByText("init-node-exporter-1")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders pod owner as dropdown title", () => {
|
|
||||||
const tabData = getFewPodsTabData();
|
|
||||||
const { getByText, container } = render(getComponent(tabData));
|
|
||||||
const podSelector: HTMLElement = container.querySelector(".pod-selector");
|
|
||||||
|
|
||||||
selectEvent.openMenu(podSelector);
|
|
||||||
|
|
||||||
expect(getByText("super-deployment")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -18,7 +18,7 @@ import { DockTabs } from "./dock-tabs";
|
|||||||
import { DockStore, DockTab, TabKind } from "./dock-store/dock.store";
|
import { DockStore, DockTab, TabKind } from "./dock-store/dock.store";
|
||||||
import { EditResource } from "./edit-resource";
|
import { EditResource } from "./edit-resource";
|
||||||
import { InstallChart } from "./install-chart";
|
import { InstallChart } from "./install-chart";
|
||||||
import { Logs } from "./logs";
|
import { LogsDockTab } from "./logs/dock-tab";
|
||||||
import { TerminalWindow } from "./terminal-window";
|
import { TerminalWindow } from "./terminal-window";
|
||||||
import { UpgradeChart } from "./upgrade-chart";
|
import { UpgradeChart } from "./upgrade-chart";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
@ -85,7 +85,7 @@ class NonInjectedDock extends React.Component<Props & Dependencies> {
|
|||||||
case TabKind.UPGRADE_CHART:
|
case TabKind.UPGRADE_CHART:
|
||||||
return <UpgradeChart tab={tab} />;
|
return <UpgradeChart tab={tab} />;
|
||||||
case TabKind.POD_LOGS:
|
case TabKind.POD_LOGS:
|
||||||
return <Logs />;
|
return <LogsDockTab tab={tab} />;
|
||||||
case TabKind.TERMINAL:
|
case TabKind.TERMINAL:
|
||||||
return <TerminalWindow tab={tab} />;
|
return <TerminalWindow tab={tab} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
|
||||||
import logStoreInjectable from "./log-store.injectable";
|
|
||||||
|
|
||||||
const reloadedLogStoreInjectable = getInjectable({
|
|
||||||
instantiate: async (di) => {
|
|
||||||
const nonReloadedStore = di.inject(logStoreInjectable);
|
|
||||||
|
|
||||||
await nonReloadedStore.reload();
|
|
||||||
|
|
||||||
return nonReloadedStore;
|
|
||||||
},
|
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.transient,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default reloadedLogStoreInjectable;
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { boundMethod } from "../../utils";
|
|
||||||
import { InfoPanel } from "./info-panel";
|
|
||||||
import { LogResourceSelector } from "./log-resource-selector";
|
|
||||||
import { LogList, NonInjectedLogList } from "./log-list";
|
|
||||||
import { LogSearch } from "./log-search";
|
|
||||||
import { LogControls } from "./log-controls";
|
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
|
||||||
import type { SearchStore } from "../../search-store/search-store";
|
|
||||||
import searchStoreInjectable from "../../search-store/search-store.injectable";
|
|
||||||
import { Spinner } from "../spinner";
|
|
||||||
import logsViewModelInjectable from "./logs/logs-view-model/logs-view-model.injectable";
|
|
||||||
import type { LogsViewModel } from "./logs/logs-view-model/logs-view-model";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
searchStore: SearchStore
|
|
||||||
model: LogsViewModel
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
class NonInjectedLogs extends React.Component<Props & Dependencies> {
|
|
||||||
private logListElement = React.createRef<NonInjectedLogList>(); // A reference for VirtualList component
|
|
||||||
|
|
||||||
get model() {
|
|
||||||
return this.props.model;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scrolling to active overlay (search word highlight)
|
|
||||||
*/
|
|
||||||
@boundMethod
|
|
||||||
scrollToOverlay() {
|
|
||||||
const { activeOverlayLine } = this.props.searchStore;
|
|
||||||
|
|
||||||
if (!this.logListElement.current || activeOverlayLine === undefined) return;
|
|
||||||
// Scroll vertically
|
|
||||||
this.logListElement.current.scrollToItem(activeOverlayLine, "center");
|
|
||||||
// Scroll horizontally in timeout since virtual list need some time to prepare its contents
|
|
||||||
setTimeout(() => {
|
|
||||||
const overlay = document.querySelector(".PodLogs .list span.active");
|
|
||||||
|
|
||||||
if (!overlay) return;
|
|
||||||
overlay.scrollIntoViewIfNeeded();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderResourceSelector() {
|
|
||||||
const { tabs, logs, logsWithoutTimestamps, saveTab, tabId } = this.model;
|
|
||||||
|
|
||||||
if (!tabs) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchLogs = tabs.showTimestamps ? logs : logsWithoutTimestamps;
|
|
||||||
|
|
||||||
const controls = (
|
|
||||||
<div className="flex gaps">
|
|
||||||
<LogResourceSelector
|
|
||||||
tabId={tabId}
|
|
||||||
tabData={tabs}
|
|
||||||
save={saveTab}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LogSearch
|
|
||||||
onSearch={this.scrollToOverlay}
|
|
||||||
logs={searchLogs}
|
|
||||||
toPrevOverlay={this.scrollToOverlay}
|
|
||||||
toNextOverlay={this.scrollToOverlay}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InfoPanel
|
|
||||||
tabId={this.model.tabId}
|
|
||||||
controls={controls}
|
|
||||||
showSubmitClose={false}
|
|
||||||
showButtons={false}
|
|
||||||
showStatusPanel={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { logs, tabs, tabId, saveTab } = this.model;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="PodLogs flex column">
|
|
||||||
{this.renderResourceSelector()}
|
|
||||||
|
|
||||||
<LogList
|
|
||||||
logs={logs}
|
|
||||||
id={tabId}
|
|
||||||
ref={this.logListElement}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LogControls
|
|
||||||
logs={logs}
|
|
||||||
tabData={tabs}
|
|
||||||
save={saveTab}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const Logs = withInjectables<Dependencies, Props>(
|
|
||||||
NonInjectedLogs,
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
getPlaceholder: () => (
|
|
||||||
<div className="flex box grow align-center justify-center">
|
|
||||||
<Spinner center />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
|
|
||||||
getProps: async (di, props) => ({
|
|
||||||
searchStore: di.inject(searchStoreInjectable),
|
|
||||||
model: await di.inject(logsViewModelInjectable),
|
|
||||||
...props,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
|
import * as selectEvent from "react-select-event";
|
||||||
|
import { Pod } from "../../../../../common/k8s-api/endpoints";
|
||||||
|
import { LogResourceSelector } from "../resource-selector";
|
||||||
|
import { dockerPod, deploymentPod1 } from "./pod.mock";
|
||||||
|
import { ThemeStore } from "../../../../theme.store";
|
||||||
|
import { UserStore } from "../../../../../common/user-store";
|
||||||
|
import mockFs from "mock-fs";
|
||||||
|
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
|
||||||
|
import type { DiRender } from "../../../test-utils/renderFor";
|
||||||
|
import { renderFor } from "../../../test-utils/renderFor";
|
||||||
|
import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
import callForLogsInjectable from "../call-for-logs.injectable";
|
||||||
|
import { LogTabViewModel, LogTabViewModelDependencies } from "../logs-view-model";
|
||||||
|
import type { TabId } from "../../dock-store/dock.store";
|
||||||
|
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
getVersion: () => "99.99.99",
|
||||||
|
getName: () => "lens",
|
||||||
|
setName: jest.fn(),
|
||||||
|
setPath: jest.fn(),
|
||||||
|
getPath: () => "tmp",
|
||||||
|
getLocale: () => "en",
|
||||||
|
setLoginItemSettings: jest.fn(),
|
||||||
|
},
|
||||||
|
ipcMain: {
|
||||||
|
on: jest.fn(),
|
||||||
|
handle: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getComponent = (model: LogTabViewModel) => (
|
||||||
|
<LogResourceSelector model={model} />
|
||||||
|
);
|
||||||
|
|
||||||
|
function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependencies>): LogTabViewModel {
|
||||||
|
return new LogTabViewModel(tabId, {
|
||||||
|
getLogs: jest.fn(),
|
||||||
|
getLogsWithoutTimestamps: jest.fn(),
|
||||||
|
getTimestampSplitLogs: jest.fn(),
|
||||||
|
getLogTabData: jest.fn(),
|
||||||
|
setLogTabData: jest.fn(),
|
||||||
|
loadLogs: jest.fn(),
|
||||||
|
reloadLogs: jest.fn(),
|
||||||
|
updateTabName: jest.fn(),
|
||||||
|
stopLoadingLogs: jest.fn(),
|
||||||
|
...deps,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOnePodViewModel = (tabId: TabId): LogTabViewModel => {
|
||||||
|
const selectedPod = new Pod(dockerPod);
|
||||||
|
|
||||||
|
return mockLogTabViewModel(tabId, {
|
||||||
|
getLogTabData: () => ({
|
||||||
|
pods: [selectedPod],
|
||||||
|
selectedPod,
|
||||||
|
selectedContainer: selectedPod.getContainers()[0],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFewPodsTabData = (tabId: TabId): LogTabViewModel => {
|
||||||
|
const selectedPod = new Pod(deploymentPod1);
|
||||||
|
const anotherPod = new Pod(dockerPod);
|
||||||
|
|
||||||
|
return mockLogTabViewModel(tabId, {
|
||||||
|
getLogTabData: () => ({
|
||||||
|
pods: [selectedPod, anotherPod],
|
||||||
|
selectedPod,
|
||||||
|
selectedContainer: selectedPod.getContainers()[0],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("<LogResourceSelector />", () => {
|
||||||
|
let render: DiRender;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||||
|
di.override(callForLogsInjectable, () => () => Promise.resolve("some-logs"));
|
||||||
|
|
||||||
|
render = renderFor(di);
|
||||||
|
|
||||||
|
await di.runSetups();
|
||||||
|
|
||||||
|
mockFs({
|
||||||
|
"tmp": {},
|
||||||
|
});
|
||||||
|
|
||||||
|
UserStore.createInstance();
|
||||||
|
ThemeStore.createInstance();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
UserStore.resetInstance();
|
||||||
|
ThemeStore.resetInstance();
|
||||||
|
mockFs.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders w/o errors", () => {
|
||||||
|
const model = getOnePodViewModel("foobar");
|
||||||
|
const { container } = render(getComponent(model));
|
||||||
|
|
||||||
|
expect(container).toBeInstanceOf(HTMLElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders proper namespace", async () => {
|
||||||
|
const model = getOnePodViewModel("foobar");
|
||||||
|
const { findByTestId } = render(getComponent(model));
|
||||||
|
const ns = await findByTestId("namespace-badge");
|
||||||
|
|
||||||
|
expect(ns).toHaveTextContent("default");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders proper selected items within dropdowns", async () => {
|
||||||
|
const model = getOnePodViewModel("foobar");
|
||||||
|
const { findByText } = render(getComponent(model));
|
||||||
|
|
||||||
|
expect(await findByText("dockerExporter")).toBeInTheDocument();
|
||||||
|
expect(await findByText("docker-exporter")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders sibling pods in dropdown", async () => {
|
||||||
|
const model = getFewPodsTabData("foobar");
|
||||||
|
const { container, findByText } = render(getComponent(model));
|
||||||
|
|
||||||
|
selectEvent.openMenu(container.querySelector(".pod-selector"));
|
||||||
|
|
||||||
|
expect(await findByText("dockerExporter", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument();
|
||||||
|
expect(await findByText("deploymentPod1", { selector: ".pod-selector-menu .Select__option" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders sibling containers in dropdown", async () => {
|
||||||
|
const model = getFewPodsTabData("foobar");
|
||||||
|
const { findByText, container } = render(getComponent(model));
|
||||||
|
const containerSelector: HTMLElement = container.querySelector(".container-selector");
|
||||||
|
|
||||||
|
selectEvent.openMenu(containerSelector);
|
||||||
|
|
||||||
|
expect(await findByText("node-exporter-1")).toBeInTheDocument();
|
||||||
|
expect(await findByText("init-node-exporter")).toBeInTheDocument();
|
||||||
|
expect(await findByText("init-node-exporter-1")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders pod owner as dropdown title", async () => {
|
||||||
|
const model = getFewPodsTabData("foobar");
|
||||||
|
const { findByText, container } = render(getComponent(model));
|
||||||
|
const podSelector: HTMLElement = container.querySelector(".pod-selector");
|
||||||
|
|
||||||
|
selectEvent.openMenu(podSelector);
|
||||||
|
|
||||||
|
expect(await findByText("super-deployment")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -3,19 +3,19 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { podsStore } from "../../+workloads-pods/pods.store";
|
import { podsStore } from "../../../+workloads-pods/pods.store";
|
||||||
import { UserStore } from "../../../../common/user-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 { ThemeStore } from "../../../../theme.store";
|
||||||
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
|
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
|
||||||
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
import { mockWindow } from "../../../../../../__mocks__/windowMock";
|
||||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../../getDiForUnitTesting";
|
||||||
import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable";
|
import logTabStoreInjectable from "../tab-store.injectable";
|
||||||
import type { LogTabStore } from "../log-tab-store/log-tab.store";
|
import type { LogTabStore } from "../tab.store";
|
||||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
import dockStoreInjectable from "../../dock-store/dock-store.injectable";
|
||||||
import type { DockStore } from "../dock-store/dock.store";
|
import type { DockStore } from "../../dock-store/dock.store";
|
||||||
import directoryForUserDataInjectable
|
import directoryForUserDataInjectable
|
||||||
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
|
|
||||||
mockWindow();
|
mockWindow();
|
||||||
@ -6,7 +6,7 @@ import React from "react";
|
|||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
import { fireEvent, render } from "@testing-library/react";
|
import { fireEvent, render } from "@testing-library/react";
|
||||||
import { ToBottom } from "../to-bottom";
|
import { ToBottom } from "../to-bottom";
|
||||||
import { noop } from "../../../utils";
|
import { noop } from "../../../../utils";
|
||||||
|
|
||||||
describe("<ToBottom/>", () => {
|
describe("<ToBottom/>", () => {
|
||||||
it("renders w/o errors", () => {
|
it("renders w/o errors", () => {
|
||||||
@ -3,7 +3,7 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import { podsApi } from "../../../../../common/k8s-api/endpoints";
|
import { podsApi } from "../../../../common/k8s-api/endpoints";
|
||||||
|
|
||||||
const callForLogsInjectable = getInjectable({
|
const callForLogsInjectable = getInjectable({
|
||||||
instantiate: () => podsApi.getLogs,
|
instantiate: () => podsApi.getLogs,
|
||||||
@ -3,53 +3,47 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./log-controls.scss";
|
import "./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 { Pod } from "../../../../common/k8s-api/endpoints";
|
||||||
import { cssNames, saveFileDialog } from "../../utils";
|
import { cssNames, saveFileDialog } from "../../../utils";
|
||||||
import { Checkbox } from "../checkbox";
|
import { Checkbox } from "../../checkbox";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../../icon";
|
||||||
import type { LogTabData } from "./log-tab-store/log-tab.store";
|
import type { LogTabViewModel } from "./logs-view-model";
|
||||||
import type { LogStore } from "./log-store/log.store";
|
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
|
||||||
import logStoreInjectable from "./log-store/log-store.injectable";
|
|
||||||
|
|
||||||
interface Props {
|
export interface LogControlsProps {
|
||||||
tabData?: LogTabData
|
model: LogTabViewModel;
|
||||||
logs: string[]
|
|
||||||
save: (data: Partial<LogTabData>) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
export const LogControls = observer(({ model }: LogControlsProps) => {
|
||||||
logStore: LogStore
|
const tabData = model.logTabData.get();
|
||||||
}
|
|
||||||
|
|
||||||
const NonInjectedLogControls = observer((props: Props & Dependencies) => {
|
|
||||||
const { tabData, save, logs, logStore } = props;
|
|
||||||
|
|
||||||
if (!tabData) {
|
if (!tabData) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logs = model.timestampSplitLogs.get();
|
||||||
const { showTimestamps, previous } = tabData;
|
const { showTimestamps, previous } = tabData;
|
||||||
const since = logs.length ? logStore.getTimestamps(logs[0]) : null;
|
const since = logs.length ? logs[0][0] : null;
|
||||||
const pod = new Pod(tabData.selectedPod);
|
const pod = new Pod(tabData.selectedPod);
|
||||||
|
|
||||||
const toggleTimestamps = () => {
|
const toggleTimestamps = () => {
|
||||||
save({ showTimestamps: !showTimestamps });
|
model.updateLogTabData({ showTimestamps: !showTimestamps });
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePrevious = () => {
|
const togglePrevious = () => {
|
||||||
save({ previous: !previous });
|
model.updateLogTabData({ previous: !previous });
|
||||||
logStore.reload();
|
model.reloadLogs();
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadLogs = () => {
|
const downloadLogs = () => {
|
||||||
const fileName = pod.getName();
|
const fileName = pod.getName();
|
||||||
const logsToDownload = showTimestamps ? logs : logStore.logsWithoutTimestamps;
|
const logsToDownload: string[] = showTimestamps
|
||||||
|
? model.logs.get()
|
||||||
|
: model.logsWithoutTimestamps.get();
|
||||||
|
|
||||||
saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
|
saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
|
||||||
};
|
};
|
||||||
@ -87,15 +81,3 @@ const NonInjectedLogControls = observer((props: Props & Dependencies) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LogControls = withInjectables<Dependencies, Props>(
|
|
||||||
NonInjectedLogControls,
|
|
||||||
|
|
||||||
{
|
|
||||||
getProps: (di, props) => ({
|
|
||||||
logStore: di.inject(logStoreInjectable),
|
|
||||||
...props,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
101
src/renderer/components/dock/logs/dock-tab.tsx
Normal file
101
src/renderer/components/dock/logs/dock-tab.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { boundMethod } from "../../../utils";
|
||||||
|
import { InfoPanel } from "../info-panel";
|
||||||
|
import { LogResourceSelector } from "./resource-selector";
|
||||||
|
import { LogList } from "./list";
|
||||||
|
import { LogSearch } from "./search";
|
||||||
|
import { LogControls } from "./controls";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import logsViewModelInjectable from "./logs-view-model.injectable";
|
||||||
|
import type { LogTabViewModel } from "./logs-view-model";
|
||||||
|
import type { DockTab } from "../dock-store/dock.store";
|
||||||
|
|
||||||
|
export interface LogsDockTabProps {
|
||||||
|
className?: string;
|
||||||
|
tab: DockTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
model: LogTabViewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class NonInjectedLogsDockTab extends React.Component<LogsDockTabProps & Dependencies> {
|
||||||
|
private logListElement = React.createRef<LogList>(); // A reference for VirtualList component
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
this.props.model.reloadLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
this.props.model.stopLoadingLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolling to active overlay (search word highlight)
|
||||||
|
*/
|
||||||
|
@boundMethod
|
||||||
|
scrollToOverlay() {
|
||||||
|
const { activeOverlayLine } = this.props.model.searchStore;
|
||||||
|
|
||||||
|
if (!this.logListElement.current || activeOverlayLine === undefined) return;
|
||||||
|
// Scroll vertically
|
||||||
|
this.logListElement.current.scrollToItem(activeOverlayLine, "center");
|
||||||
|
// Scroll horizontally in timeout since virtual list need some time to prepare its contents
|
||||||
|
setTimeout(() => {
|
||||||
|
const overlay = document.querySelector(".PodLogs .list span.active");
|
||||||
|
|
||||||
|
if (!overlay) return;
|
||||||
|
overlay.scrollIntoViewIfNeeded();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { model, tab } = this.props;
|
||||||
|
const { logTabData } = model;
|
||||||
|
const data = logTabData.get();
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="PodLogs flex column">
|
||||||
|
<InfoPanel
|
||||||
|
tabId={tab.id}
|
||||||
|
controls={(
|
||||||
|
<div className="flex gaps">
|
||||||
|
<LogResourceSelector model={model} />
|
||||||
|
<LogSearch
|
||||||
|
onSearch={this.scrollToOverlay}
|
||||||
|
model={model}
|
||||||
|
toPrevOverlay={this.scrollToOverlay}
|
||||||
|
toNextOverlay={this.scrollToOverlay}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
showSubmitClose={false}
|
||||||
|
showButtons={false}
|
||||||
|
showStatusPanel={false}
|
||||||
|
/>
|
||||||
|
<LogList model={model} ref={this.logListElement} />
|
||||||
|
<LogControls model={model} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogsDockTab = withInjectables<Dependencies, LogsDockTabProps>(NonInjectedLogsDockTab, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
model: di.inject(logsViewModelInjectable, {
|
||||||
|
tabId: props.tab.id,
|
||||||
|
}),
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import logTabStoreInjectable from "./tab-store.injectable";
|
||||||
|
|
||||||
|
const getLogTabDataInjectable = getInjectable({
|
||||||
|
instantiate: (di) => di.inject(logTabStoreInjectable).getData,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getLogTabDataInjectable;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import logStoreInjectable from "./store.injectable";
|
||||||
|
|
||||||
|
const getLogsWithoutTimestampsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => di.inject(logStoreInjectable).getLogsWithoutTimestampsByTabId,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getLogsWithoutTimestampsInjectable;
|
||||||
13
src/renderer/components/dock/logs/get-logs.injectable.ts
Normal file
13
src/renderer/components/dock/logs/get-logs.injectable.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import logStoreInjectable from "./store.injectable";
|
||||||
|
|
||||||
|
const getLogsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => di.inject(logStoreInjectable).getLogsByTabId,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getLogsInjectable;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import logStoreInjectable from "./store.injectable";
|
||||||
|
|
||||||
|
const getTimestampSplitLogsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => di.inject(logStoreInjectable).getTimestampSplitLogsByTabId,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default getTimestampSplitLogsInjectable;
|
||||||
@ -3,7 +3,7 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./log-list.scss";
|
import "./list.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import AnsiUp from "ansi_up";
|
import AnsiUp from "ansi_up";
|
||||||
@ -13,33 +13,21 @@ import { action, computed, observable, makeObservable, reaction } from "mobx";
|
|||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import type { Align, ListOnScrollProps } from "react-window";
|
import type { Align, ListOnScrollProps } from "react-window";
|
||||||
import { SearchStore } from "../../search-store/search-store";
|
import { SearchStore } from "../../../search-store/search-store";
|
||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../../common/user-store";
|
||||||
import { array, boundMethod, cssNames } from "../../utils";
|
import { array, boundMethod, cssNames } from "../../../utils";
|
||||||
import { VirtualList } from "../virtual-list";
|
import { VirtualList } from "../../virtual-list";
|
||||||
import type { LogStore } from "./log-store/log.store";
|
|
||||||
import type { LogTabStore } from "./log-tab-store/log-tab.store";
|
|
||||||
import { ToBottom } from "./to-bottom";
|
import { ToBottom } from "./to-bottom";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import type { LogTabViewModel } from "../logs/logs-view-model";
|
||||||
import logTabStoreInjectable from "./log-tab-store/log-tab-store.injectable";
|
|
||||||
import logStoreInjectable from "./log-store/log-store.injectable";
|
|
||||||
import searchStoreInjectable from "../../search-store/search-store.injectable";
|
|
||||||
|
|
||||||
interface Props {
|
export interface LogListProps {
|
||||||
logs: string[]
|
model: LogTabViewModel;
|
||||||
id: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorConverter = new AnsiUp();
|
const colorConverter = new AnsiUp();
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
logTabStore: LogTabStore
|
|
||||||
logStore: LogStore
|
|
||||||
searchStore: SearchStore
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
export class LogList extends React.Component<LogListProps> {
|
||||||
@observable isJumpButtonVisible = false;
|
@observable isJumpButtonVisible = false;
|
||||||
@observable isLastLineVisible = true;
|
@observable isLastLineVisible = true;
|
||||||
|
|
||||||
@ -47,16 +35,18 @@ export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
|||||||
private virtualListRef = React.createRef<VirtualList>(); // A reference for VirtualList component
|
private virtualListRef = React.createRef<VirtualList>(); // A reference for VirtualList component
|
||||||
private lineHeight = 18; // Height of a log line. Should correlate with styles in pod-log-list.scss
|
private lineHeight = 18; // Height of a log line. Should correlate with styles in pod-log-list.scss
|
||||||
|
|
||||||
constructor(props: Props & Dependencies) {
|
constructor(props: LogListProps) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => this.props.logs, this.onLogsInitialLoad),
|
reaction(() => this.props.model.logs.get(), (logs, prevLogs) => {
|
||||||
reaction(() => this.props.logs, this.onLogsUpdate),
|
this.onLogsInitialLoad(logs, prevLogs);
|
||||||
reaction(() => this.props.logs, this.onUserScrolledUp),
|
this.onLogsUpdate();
|
||||||
|
this.onUserScrolledUp(logs, prevLogs);
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +75,7 @@ export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
|||||||
|
|
||||||
if (newLogsAdded && scrolledToBeginning) {
|
if (newLogsAdded && scrolledToBeginning) {
|
||||||
const firstLineContents = prevLogs[0];
|
const firstLineContents = prevLogs[0];
|
||||||
const lineToScroll = this.props.logs.findIndex((value) => value == firstLineContents);
|
const lineToScroll = logs.findIndex((value) => value == firstLineContents);
|
||||||
|
|
||||||
if (lineToScroll !== -1) {
|
if (lineToScroll !== -1) {
|
||||||
this.scrollToItem(lineToScroll, "start");
|
this.scrollToItem(lineToScroll, "start");
|
||||||
@ -97,15 +87,15 @@ export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
|||||||
* Returns logs with or without timestamps regarding to showTimestamps prop
|
* Returns logs with or without timestamps regarding to showTimestamps prop
|
||||||
*/
|
*/
|
||||||
@computed
|
@computed
|
||||||
get logs() {
|
get logs(): string[] {
|
||||||
const showTimestamps = this.props.logTabStore.getData(this.props.id)?.showTimestamps;
|
const { showTimestamps } = this.props.model.logTabData.get();
|
||||||
|
|
||||||
if (!showTimestamps) {
|
if (!showTimestamps) {
|
||||||
return this.props.logStore.logsWithoutTimestamps;
|
return this.props.model.logsWithoutTimestamps.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.props.logs
|
return this.props.model.timestampSplitLogs
|
||||||
.map(log => this.props.logStore.splitOutTimestamp(log))
|
.get()
|
||||||
.map(([logTimestamp, log]) => (`${logTimestamp && moment.tz(logTimestamp, UserStore.getInstance().localeTimezone).format()}${log}`));
|
.map(([logTimestamp, log]) => (`${logTimestamp && moment.tz(logTimestamp, UserStore.getInstance().localeTimezone).format()}${log}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +136,7 @@ export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
|||||||
const { scrollOffset } = props;
|
const { scrollOffset } = props;
|
||||||
|
|
||||||
if (scrollOffset === 0) {
|
if (scrollOffset === 0) {
|
||||||
this.props.logStore.load();
|
this.props.model.loadLogs();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -177,7 +167,7 @@ export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
|||||||
* @returns A react element with a row itself
|
* @returns A react element with a row itself
|
||||||
*/
|
*/
|
||||||
getLogRow = (rowIndex: number) => {
|
getLogRow = (rowIndex: number) => {
|
||||||
const { searchQuery, isActiveOverlay } = this.props.searchStore;
|
const { searchQuery, isActiveOverlay } = this.props.model.searchStore;
|
||||||
const item = this.logs[rowIndex];
|
const item = this.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));
|
||||||
@ -250,17 +240,3 @@ export class NonInjectedLogList extends React.Component<Props & Dependencies> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LogList = withInjectables<Dependencies, Props>(
|
|
||||||
NonInjectedLogList,
|
|
||||||
|
|
||||||
{
|
|
||||||
getProps: (di, props) => ({
|
|
||||||
logTabStore: di.inject(logTabStoreInjectable),
|
|
||||||
logStore: di.inject(logStoreInjectable),
|
|
||||||
searchStore: di.inject(searchStoreInjectable),
|
|
||||||
...props,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
13
src/renderer/components/dock/logs/load-logs.injectable.ts
Normal file
13
src/renderer/components/dock/logs/load-logs.injectable.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import logStoreInjectable from "./store.injectable";
|
||||||
|
|
||||||
|
const loadLogsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => di.inject(logStoreInjectable).load,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default loadLogsInjectable;
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { LogTabViewModel } from "./logs-view-model";
|
||||||
|
import type { TabId } from "../dock-store/dock.store";
|
||||||
|
import getLogsInjectable from "./get-logs.injectable";
|
||||||
|
import getLogsWithoutTimestampsInjectable from "./get-logs-without-timestamps.injectable";
|
||||||
|
import getTimestampSplitLogsInjectable from "./get-timestamp-split-logs.injectable";
|
||||||
|
import reloadLoadsInjectable from "./reload-logs.injectable";
|
||||||
|
import getLogTabDataInjectable from "./get-log-tab-data.injectable";
|
||||||
|
import loadLogsInjectable from "./load-logs.injectable";
|
||||||
|
import setLogTabDataInjectable from "./set-log-tab-data.injectable";
|
||||||
|
import updateTabNameInjectable from "./update-tab-name.injectable";
|
||||||
|
import stopLoadingLogsInjectable from "./stop-loading-logs.injectable";
|
||||||
|
|
||||||
|
export interface InstantiateArgs {
|
||||||
|
tabId: TabId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logsViewModelInjectable = getInjectable({
|
||||||
|
instantiate: (di, { tabId }: InstantiateArgs) => new LogTabViewModel(tabId, {
|
||||||
|
getLogs: di.inject(getLogsInjectable),
|
||||||
|
getLogsWithoutTimestamps: di.inject(getLogsWithoutTimestampsInjectable),
|
||||||
|
getTimestampSplitLogs: di.inject(getTimestampSplitLogsInjectable),
|
||||||
|
reloadLogs: di.inject(reloadLoadsInjectable),
|
||||||
|
getLogTabData: di.inject(getLogTabDataInjectable),
|
||||||
|
setLogTabData: di.inject(setLogTabDataInjectable),
|
||||||
|
loadLogs: di.inject(loadLogsInjectable),
|
||||||
|
updateTabName: di.inject(updateTabNameInjectable),
|
||||||
|
stopLoadingLogs: di.inject(stopLoadingLogsInjectable),
|
||||||
|
}),
|
||||||
|
lifecycle: lifecycleEnum.transient,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default logsViewModelInjectable;
|
||||||
39
src/renderer/components/dock/logs/logs-view-model.ts
Normal file
39
src/renderer/components/dock/logs/logs-view-model.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import type { LogTabData } from "./tab.store";
|
||||||
|
import { computed, IComputedValue } from "mobx";
|
||||||
|
import type { TabId } from "../dock-store/dock.store";
|
||||||
|
import { SearchStore } from "../../../search-store/search-store";
|
||||||
|
|
||||||
|
export interface LogTabViewModelDependencies {
|
||||||
|
getLogs: (tabId: TabId) => string[];
|
||||||
|
getLogsWithoutTimestamps: (tabId: TabId) => string[];
|
||||||
|
getTimestampSplitLogs: (tabId: TabId) => [string, string][];
|
||||||
|
getLogTabData: (tabId: TabId) => LogTabData;
|
||||||
|
setLogTabData: (tabId: TabId, data: LogTabData) => void;
|
||||||
|
loadLogs: (tabId: TabId, logTabData: IComputedValue<LogTabData>) => Promise<void>;
|
||||||
|
reloadLogs: (tabId: TabId, logTabData: IComputedValue<LogTabData>) => Promise<void>;
|
||||||
|
updateTabName: (tabId: TabId) => void;
|
||||||
|
stopLoadingLogs: (tabId: TabId) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LogTabViewModel {
|
||||||
|
constructor(protected readonly tabId: TabId, private readonly dependencies: LogTabViewModelDependencies) {}
|
||||||
|
|
||||||
|
readonly logs = computed(() => this.dependencies.getLogs(this.tabId));
|
||||||
|
readonly logsWithoutTimestamps = computed(() => this.dependencies.getLogsWithoutTimestamps(this.tabId));
|
||||||
|
readonly timestampSplitLogs = computed(() => this.dependencies.getTimestampSplitLogs(this.tabId));
|
||||||
|
readonly logTabData = computed(() => this.dependencies.getLogTabData(this.tabId));
|
||||||
|
readonly searchStore = new SearchStore();
|
||||||
|
|
||||||
|
updateLogTabData = (partialData: Partial<LogTabData>) => {
|
||||||
|
this.dependencies.setLogTabData(this.tabId, { ...this.logTabData.get(), ...partialData });
|
||||||
|
};
|
||||||
|
|
||||||
|
loadLogs = () => this.dependencies.loadLogs(this.tabId, this.logTabData);
|
||||||
|
reloadLogs = () => this.dependencies.reloadLogs(this.tabId, this.logTabData);
|
||||||
|
updateTabName = () => this.dependencies.updateTabName(this.tabId);
|
||||||
|
stopLoadingLogs = () => this.dependencies.stopLoadingLogs(this.tabId);
|
||||||
|
}
|
||||||
@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
|
||||||
import dockStoreInjectable from "../../dock-store/dock-store.injectable";
|
|
||||||
import logTabStoreInjectable from "../../log-tab-store/log-tab-store.injectable";
|
|
||||||
import reloadedLogStoreInjectable from "../../log-store/reloaded-log-store.injectable";
|
|
||||||
import { LogsViewModel } from "./logs-view-model";
|
|
||||||
|
|
||||||
const logsViewModelInjectable = getInjectable({
|
|
||||||
instantiate: async (di) => new LogsViewModel({
|
|
||||||
dockStore: di.inject(dockStoreInjectable),
|
|
||||||
logTabStore: di.inject(logTabStoreInjectable),
|
|
||||||
logStore: await di.inject(reloadedLogStoreInjectable),
|
|
||||||
}),
|
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.singleton,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default logsViewModelInjectable;
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import type { LogTabData, LogTabStore } from "../../log-tab-store/log-tab.store";
|
|
||||||
import type { LogStore } from "../../log-store/log.store";
|
|
||||||
import { computed, makeObservable } from "mobx";
|
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
dockStore: { selectedTabId: string },
|
|
||||||
logTabStore: LogTabStore
|
|
||||||
logStore: LogStore
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LogsViewModel {
|
|
||||||
constructor(private dependencies: Dependencies) {
|
|
||||||
makeObservable(this, {
|
|
||||||
logs: computed,
|
|
||||||
logsWithoutTimestamps: computed,
|
|
||||||
tabs: computed,
|
|
||||||
tabId: computed,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get logs() {
|
|
||||||
return this.dependencies.logStore.logs;
|
|
||||||
}
|
|
||||||
|
|
||||||
get logsWithoutTimestamps() {
|
|
||||||
return this.dependencies.logStore.logsWithoutTimestamps;
|
|
||||||
}
|
|
||||||
|
|
||||||
get tabs() {
|
|
||||||
return this.dependencies.logTabStore.tabs;
|
|
||||||
}
|
|
||||||
|
|
||||||
get tabId() {
|
|
||||||
return this.dependencies.dockStore.selectedTabId;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveTab = (newTabs: LogTabData) => {
|
|
||||||
this.dependencies.logTabStore.setData(this.tabId, { ...this.tabs, ...newTabs });
|
|
||||||
};
|
|
||||||
}
|
|
||||||
13
src/renderer/components/dock/logs/reload-logs.injectable.ts
Normal file
13
src/renderer/components/dock/logs/reload-logs.injectable.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import logStoreInjectable from "./store.injectable";
|
||||||
|
|
||||||
|
const reloadLoadsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => di.inject(logStoreInjectable).reload,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default reloadLoadsInjectable;
|
||||||
@ -3,55 +3,48 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./log-resource-selector.scss";
|
import "./resource-selector.scss";
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
|
||||||
import { Pod } from "../../../common/k8s-api/endpoints";
|
import { Pod } from "../../../../common/k8s-api/endpoints";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../../badge";
|
||||||
import { Select, SelectOption } from "../select";
|
import { Select, SelectOption } from "../../select";
|
||||||
import type { LogTabData, LogTabStore } from "./log-tab-store/log-tab.store";
|
import { podsStore } from "../../+workloads-pods/pods.store";
|
||||||
import { podsStore } from "../+workloads-pods/pods.store";
|
import type { LogTabViewModel } from "./logs-view-model";
|
||||||
import type { TabId } from "./dock-store/dock.store";
|
|
||||||
import logTabStoreInjectable from "./log-tab-store/log-tab-store.injectable";
|
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
|
||||||
import logStoreInjectable from "./log-store/log-store.injectable";
|
|
||||||
|
|
||||||
interface Props {
|
export interface LogResourceSelectorProps {
|
||||||
tabId: TabId
|
model: LogTabViewModel;
|
||||||
tabData: LogTabData
|
|
||||||
save: (data: Partial<LogTabData>) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
export const LogResourceSelector = observer(({ model }: LogResourceSelectorProps) => {
|
||||||
logTabStore: LogTabStore
|
const tabData = model.logTabData.get();
|
||||||
reloadLogs: () => Promise<void>
|
|
||||||
}
|
if (!tabData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) => {
|
|
||||||
const { tabData, save, tabId, logTabStore, reloadLogs } = props;
|
|
||||||
const { selectedPod, selectedContainer, pods } = tabData;
|
const { selectedPod, selectedContainer, pods } = tabData;
|
||||||
const pod = new Pod(selectedPod);
|
const pod = new Pod(selectedPod);
|
||||||
const containers = pod.getContainers();
|
const containers = pod.getContainers();
|
||||||
const initContainers = pod.getInitContainers();
|
const initContainers = pod.getInitContainers();
|
||||||
|
|
||||||
const onContainerChange = (option: SelectOption) => {
|
const onContainerChange = (option: SelectOption) => {
|
||||||
save({
|
model.updateLogTabData({
|
||||||
selectedContainer: containers
|
selectedContainer: containers
|
||||||
.concat(initContainers)
|
.concat(initContainers)
|
||||||
.find(container => container.name === option.value),
|
.find(container => container.name === option.value),
|
||||||
});
|
});
|
||||||
|
|
||||||
reloadLogs();
|
model.reloadLogs();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPodChange = (option: SelectOption) => {
|
const onPodChange = (option: SelectOption) => {
|
||||||
const selectedPod = podsStore.getByName(option.value, pod.getNs());
|
const selectedPod = podsStore.getByName(option.value, pod.getNs());
|
||||||
|
|
||||||
save({ selectedPod });
|
model.updateLogTabData({ selectedPod });
|
||||||
|
model.updateTabName();
|
||||||
logTabStore.renameTab(tabId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectOptions = (items: string[]) => {
|
const getSelectOptions = (items: string[]) => {
|
||||||
@ -82,7 +75,7 @@ const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) =>
|
|||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reloadLogs();
|
model.reloadLogs();
|
||||||
}, [selectedPod]);
|
}, [selectedPod]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -95,6 +88,7 @@ const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) =>
|
|||||||
onChange={onPodChange}
|
onChange={onPodChange}
|
||||||
autoConvertOptions={false}
|
autoConvertOptions={false}
|
||||||
className="pod-selector"
|
className="pod-selector"
|
||||||
|
menuClass="pod-selector-menu"
|
||||||
/>
|
/>
|
||||||
<span>Container</span>
|
<span>Container</span>
|
||||||
<Select
|
<Select
|
||||||
@ -103,20 +97,9 @@ const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) =>
|
|||||||
onChange={onContainerChange}
|
onChange={onContainerChange}
|
||||||
autoConvertOptions={false}
|
autoConvertOptions={false}
|
||||||
className="container-selector"
|
className="container-selector"
|
||||||
|
menuClass="container-selector-menu"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LogResourceSelector = withInjectables<Dependencies, Props>(
|
|
||||||
NonInjectedLogResourceSelector,
|
|
||||||
|
|
||||||
{
|
|
||||||
getProps: (di, props) => ({
|
|
||||||
logTabStore: di.inject(logTabStoreInjectable),
|
|
||||||
reloadLogs: di.inject(logStoreInjectable).reload,
|
|
||||||
...props,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
@ -3,33 +3,33 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./log-search.scss";
|
import "./search.scss";
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { SearchInput } from "../input";
|
import { SearchInput } from "../../input";
|
||||||
import type { SearchStore } from "../../search-store/search-store";
|
import { Icon } from "../../icon";
|
||||||
import { Icon } from "../icon";
|
import type { LogTabViewModel } from "./logs-view-model";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
|
||||||
import searchStoreInjectable from "../../search-store/search-store.injectable";
|
|
||||||
|
|
||||||
export interface PodLogSearchProps {
|
export interface PodLogSearchProps {
|
||||||
onSearch: (query: string) => void
|
onSearch: (query: string) => void;
|
||||||
toPrevOverlay: () => void
|
toPrevOverlay: () => void;
|
||||||
toNextOverlay: () => void
|
toNextOverlay: () => void;
|
||||||
|
model: LogTabViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends PodLogSearchProps {
|
|
||||||
logs: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Dependencies {
|
export const LogSearch = observer(({ onSearch, toPrevOverlay, toNextOverlay, model }: PodLogSearchProps) => {
|
||||||
searchStore: SearchStore
|
const tabData = model.logTabData.get();
|
||||||
}
|
|
||||||
|
|
||||||
const NonInjectedLogSearch = observer((props: Props & Dependencies) => {
|
if (!tabData) {
|
||||||
const { logs, onSearch, toPrevOverlay, toNextOverlay, searchStore } = props;
|
return null;
|
||||||
const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore;
|
}
|
||||||
|
|
||||||
|
const logs = tabData.showTimestamps
|
||||||
|
? model.logs.get()
|
||||||
|
: model.logsWithoutTimestamps.get();
|
||||||
|
const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = model.searchStore;
|
||||||
const jumpDisabled = !searchQuery || !occurrences.length;
|
const jumpDisabled = !searchQuery || !occurrences.length;
|
||||||
const findCounts = (
|
const findCounts = (
|
||||||
<div className="find-count">
|
<div className="find-count">
|
||||||
@ -38,7 +38,7 @@ const NonInjectedLogSearch = observer((props: Props & Dependencies) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const setSearch = (query: string) => {
|
const setSearch = (query: string) => {
|
||||||
searchStore.onSearch(logs, query);
|
model.searchStore.onSearch(logs, query);
|
||||||
onSearch(query);
|
onSearch(query);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ const NonInjectedLogSearch = observer((props: Props & Dependencies) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Refresh search when logs changed
|
// Refresh search when logs changed
|
||||||
searchStore.onSearch(logs);
|
model.searchStore.onSearch(logs);
|
||||||
}, [logs]);
|
}, [logs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -92,14 +92,3 @@ const NonInjectedLogSearch = observer((props: Props & Dependencies) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LogSearch = withInjectables<Dependencies, Props>(
|
|
||||||
NonInjectedLogSearch,
|
|
||||||
|
|
||||||
{
|
|
||||||
getProps: (di, props) => ({
|
|
||||||
searchStore: di.inject(searchStoreInjectable),
|
|
||||||
...props,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import logTabStoreInjectable from "./tab-store.injectable";
|
||||||
|
|
||||||
|
const setLogTabDataInjectable = getInjectable({
|
||||||
|
instantiate: (di) => di.inject(logTabStoreInjectable).setData,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default setLogTabDataInjectable;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import logStoreInjectable from "./store.injectable";
|
||||||
|
|
||||||
|
const stopLoadingLogsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => di.inject(logStoreInjectable).stopLoadingLogs,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default stopLoadingLogsInjectable;
|
||||||
@ -3,15 +3,11 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import { LogStore } from "./log.store";
|
import { LogStore } from "./store";
|
||||||
import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable";
|
import callForLogsInjectable from "./call-for-logs.injectable";
|
||||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
|
||||||
import callForLogsInjectable from "./call-for-logs/call-for-logs.injectable";
|
|
||||||
|
|
||||||
const logStoreInjectable = getInjectable({
|
const logStoreInjectable = getInjectable({
|
||||||
instantiate: (di) => new LogStore({
|
instantiate: (di) => new LogStore({
|
||||||
logTabStore: di.inject(logTabStoreInjectable),
|
|
||||||
dockStore: di.inject(dockStoreInjectable),
|
|
||||||
callForLogs: di.inject(callForLogsInjectable),
|
callForLogs: di.inject(callForLogsInjectable),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@ -3,46 +3,28 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { autorun, computed, observable, makeObservable } from "mobx";
|
import { computed, observable, makeObservable, IComputedValue } from "mobx";
|
||||||
|
|
||||||
import { IPodLogsQuery, Pod } from "../../../../common/k8s-api/endpoints";
|
import { IPodLogsQuery, Pod } from "../../../../common/k8s-api/endpoints";
|
||||||
import { autoBind, interval } from "../../../utils";
|
import { autoBind, getOrInsertWith, interval, IntervalFn } from "../../../utils";
|
||||||
import { DockStore, TabId, TabKind } from "../dock-store/dock.store";
|
import type { TabId } from "../dock-store/dock.store";
|
||||||
import type { LogTabStore } from "../log-tab-store/log-tab.store";
|
import type { LogTabData } from "./tab.store";
|
||||||
|
|
||||||
type PodLogLine = string;
|
type PodLogLine = string;
|
||||||
|
|
||||||
const logLinesToLoad = 500;
|
const logLinesToLoad = 500;
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
logTabStore: LogTabStore
|
|
||||||
dockStore: DockStore
|
|
||||||
callForLogs: ({ namespace, name }: { namespace: string, name: string }, query: IPodLogsQuery) => Promise<string>
|
callForLogs: ({ namespace, name }: { namespace: string, name: string }, query: IPodLogsQuery) => Promise<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LogStore {
|
export class LogStore {
|
||||||
private refresher = interval(10, () => {
|
@observable protected podLogs = observable.map<TabId, PodLogLine[]>();
|
||||||
const id = this.dependencies.dockStore.selectedTabId;
|
protected refreshers = new Map<TabId, IntervalFn>();
|
||||||
|
|
||||||
if (!this.podLogs.get(id)) return;
|
|
||||||
this.loadMore(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
@observable podLogs = observable.map<TabId, PodLogLine[]>();
|
|
||||||
|
|
||||||
constructor(private dependencies: Dependencies) {
|
constructor(private dependencies: Dependencies) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
|
|
||||||
autorun(() => {
|
|
||||||
const { selectedTab, isOpen } = this.dependencies.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 {
|
||||||
@ -55,7 +37,7 @@ export class LogStore {
|
|||||||
`Reason: ${error.reason} (${error.code})`,
|
`Reason: ${error.reason} (${error.code})`,
|
||||||
];
|
];
|
||||||
|
|
||||||
this.refresher.stop();
|
this.stopLoadingLogs(tabId);
|
||||||
this.podLogs.set(tabId, message);
|
this.podLogs.set(tabId, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,35 +47,51 @@ export class LogStore {
|
|||||||
* Also, it handles loading errors, rewriting whole logs with error
|
* Also, it handles loading errors, rewriting whole logs with error
|
||||||
* messages
|
* messages
|
||||||
*/
|
*/
|
||||||
load = async () => {
|
load = async (tabId: TabId, logTabData: IComputedValue<LogTabData>) => {
|
||||||
const tabId = this.dependencies.dockStore.selectedTabId;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const logs = await this.loadLogs(tabId, {
|
const logs = await this.loadLogs(logTabData, {
|
||||||
tailLines: this.lines + logLinesToLoad,
|
tailLines: this.getLinesByTabId(tabId) + logLinesToLoad,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.refresher.start();
|
this.getRefresher(tabId, logTabData).start();
|
||||||
this.podLogs.set(tabId, logs);
|
this.podLogs.set(tabId, logs);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handlerError(tabId, error);
|
this.handlerError(tabId, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private getRefresher(tabId: TabId, logTabData: IComputedValue<LogTabData>): IntervalFn {
|
||||||
|
return getOrInsertWith(this.refreshers, tabId, () => (
|
||||||
|
interval(10, () => {
|
||||||
|
if (this.podLogs.has(tabId)) {
|
||||||
|
this.loadMore(tabId, logTabData);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop loading more logs for a given tab
|
||||||
|
* @param tabId The ID of the logs tab to stop loading more logs for
|
||||||
|
*/
|
||||||
|
public stopLoadingLogs(tabId: TabId): void {
|
||||||
|
this.refreshers.get(tabId)?.stop();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function is used to refresher/stream-like requests.
|
* Function is used to refresher/stream-like requests.
|
||||||
* It changes 'sinceTime' param each time allowing to fetch logs
|
* It changes 'sinceTime' param each time allowing to fetch logs
|
||||||
* starting from last line received.
|
* starting from last line received.
|
||||||
* @param tabId
|
* @param tabId
|
||||||
*/
|
*/
|
||||||
loadMore = async (tabId: TabId) => {
|
loadMore = async (tabId: TabId, logTabData: IComputedValue<LogTabData>) => {
|
||||||
if (!this.podLogs.get(tabId).length) {
|
if (!this.podLogs.get(tabId).length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const oldLogs = this.podLogs.get(tabId);
|
const oldLogs = this.podLogs.get(tabId);
|
||||||
const logs = await this.loadLogs(tabId, {
|
const logs = await this.loadLogs(logTabData, {
|
||||||
sinceTime: this.getLastSinceTime(tabId),
|
sinceTime: this.getLastSinceTime(tabId),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -111,11 +109,9 @@ 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[]> {
|
private async loadLogs(logTabData: IComputedValue<LogTabData>, params: Partial<IPodLogsQuery>): Promise<string[]> {
|
||||||
const data = this.dependencies.logTabStore.getData(tabId);
|
const { selectedContainer, previous, selectedPod } = logTabData.get();
|
||||||
|
const pod = new Pod(selectedPod);
|
||||||
const { selectedContainer, previous } = data;
|
|
||||||
const pod = new Pod(data.selectedPod);
|
|
||||||
const namespace = pod.getNs();
|
const namespace = pod.getNs();
|
||||||
const name = pod.getName();
|
const name = pod.getName();
|
||||||
|
|
||||||
@ -130,6 +126,7 @@ export class LogStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated This depends on dockStore, which should be removed
|
||||||
* Converts logs into a string array
|
* Converts logs into a string array
|
||||||
* @returns Length of log lines
|
* @returns Length of log lines
|
||||||
*/
|
*/
|
||||||
@ -138,21 +135,36 @@ export class LogStore {
|
|||||||
return this.logs.length;
|
return this.logs.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getLinesByTabId = (tabId: TabId): number => {
|
||||||
|
return this.getLogsByTabId(tabId).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getLogsByTabId = (tabId: TabId): string[] => {
|
||||||
|
return this.podLogs.get(tabId) ?? [];
|
||||||
|
};
|
||||||
|
|
||||||
|
public getLogsWithoutTimestampsByTabId = (tabId: TabId): string[] => {
|
||||||
|
return this.getLogsByTabId(tabId).map(this.removeTimestamps);
|
||||||
|
};
|
||||||
|
|
||||||
|
public getTimestampSplitLogsByTabId = (tabId: TabId): [string, string][] => {
|
||||||
|
return this.getLogsByTabId(tabId).map(this.splitOutTimestamp);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated This now only returns the empty array
|
||||||
* Returns logs with timestamps for selected tab
|
* Returns logs with timestamps for selected tab
|
||||||
*/
|
*/
|
||||||
@computed
|
get logs(): string[] {
|
||||||
get logs() {
|
return [];
|
||||||
return this.podLogs.get(this.dependencies.dockStore.selectedTabId) ?? [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated This now only returns the empty array
|
||||||
* Removes timestamps from each log line and returns changed logs
|
* Removes timestamps from each log line and returns changed logs
|
||||||
* @returns Logs without timestamps
|
* @returns Logs without timestamps
|
||||||
*/
|
*/
|
||||||
@computed
|
get logsWithoutTimestamps(): string[] {
|
||||||
get logsWithoutTimestamps() {
|
|
||||||
return this.logs.map(item => this.removeTimestamps(item));
|
return this.logs.map(item => this.removeTimestamps(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +183,7 @@ export class LogStore {
|
|||||||
return stamp.toISOString();
|
return stamp.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
splitOutTimestamp(logs: string): [string, string] {
|
splitOutTimestamp = (logs: string): [string, string] => {
|
||||||
const extraction = /^(\d+\S+)(.*)/m.exec(logs);
|
const extraction = /^(\d+\S+)(.*)/m.exec(logs);
|
||||||
|
|
||||||
if (!extraction || extraction.length < 3) {
|
if (!extraction || extraction.length < 3) {
|
||||||
@ -179,23 +191,23 @@ export class LogStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [extraction[1], extraction[2]];
|
return [extraction[1], extraction[2]];
|
||||||
}
|
};
|
||||||
|
|
||||||
getTimestamps(logs: string) {
|
getTimestamps(logs: string) {
|
||||||
return logs.match(/^\d+\S+/gm);
|
return logs.match(/^\d+\S+/gm);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTimestamps(logs: string) {
|
removeTimestamps = (logs: string) => {
|
||||||
return logs.replace(/^\d+.*?\s/gm, "");
|
return logs.replace(/^\d+.*?\s/gm, "");
|
||||||
}
|
};
|
||||||
|
|
||||||
clearLogs(tabId: TabId) {
|
clearLogs(tabId: TabId) {
|
||||||
this.podLogs.delete(tabId);
|
this.podLogs.delete(tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
reload = async () => {
|
reload = (tabId: TabId, logTabData: IComputedValue<LogTabData>) => {
|
||||||
this.clearLogs(this.dependencies.dockStore.selectedTabId);
|
this.clearLogs(tabId);
|
||||||
|
|
||||||
await this.load();
|
return this.load(tabId, logTabData);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -3,7 +3,7 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import { LogTabStore } from "./log-tab.store";
|
import { LogTabStore } from "./tab.store";
|
||||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||||
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||||
|
|
||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import uniqueId from "lodash/uniqueId";
|
import uniqueId from "lodash/uniqueId";
|
||||||
import { computed, makeObservable, reaction } from "mobx";
|
import { reaction } from "mobx";
|
||||||
import { podsStore } from "../../+workloads-pods/pods.store";
|
import { podsStore } from "../../+workloads-pods/pods.store";
|
||||||
|
|
||||||
import { IPodContainer, Pod } from "../../../../common/k8s-api/endpoints";
|
import { IPodContainer, Pod } from "../../../../common/k8s-api/endpoints";
|
||||||
@ -43,14 +43,6 @@ export class LogTabStore extends DockTabStore<LogTabData> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
reaction(() => podsStore.items.length, () => this.updateTabsData());
|
reaction(() => podsStore.items.length, () => this.updateTabsData());
|
||||||
|
|
||||||
makeObservable(this, {
|
|
||||||
tabs: computed,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get tabs() {
|
|
||||||
return this.data.get(this.dependencies.dockStore.selectedTabId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): string {
|
createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): string {
|
||||||
@ -81,7 +73,7 @@ export class LogTabStore extends DockTabStore<LogTabData> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renameTab(tabId: string) {
|
updateTabName(tabId: string) {
|
||||||
const { selectedPod } = this.getData(tabId);
|
const { selectedPod } = this.getData(tabId);
|
||||||
|
|
||||||
this.dependencies.dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`);
|
this.dependencies.dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`);
|
||||||
@ -128,7 +120,7 @@ export class LogTabStore extends DockTabStore<LogTabData> {
|
|||||||
pods,
|
pods,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.renameTab(tabId);
|
this.updateTabName(tabId);
|
||||||
} else {
|
} else {
|
||||||
this.closeTab(tabId);
|
this.closeTab(tabId);
|
||||||
}
|
}
|
||||||
@ -3,7 +3,7 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../../icon";
|
||||||
|
|
||||||
export function ToBottom({ onClick }: { onClick: () => void }) {
|
export function ToBottom({ onClick }: { onClick: () => void }) {
|
||||||
return (
|
return (
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import logTabStoreInjectable from "./tab-store.injectable";
|
||||||
|
|
||||||
|
const updateTabNameInjectable = getInjectable({
|
||||||
|
instantiate: (di) => di.inject(logTabStoreInjectable).updateTabName,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default updateTabNameInjectable;
|
||||||
@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
|
||||||
import dockStoreInjectable from "../components/dock/dock-store/dock-store.injectable";
|
|
||||||
import { SearchStore } from "./search-store";
|
|
||||||
|
|
||||||
const searchStoreInjectable = getInjectable({
|
|
||||||
instantiate: (di) => new SearchStore({
|
|
||||||
dockStore: di.inject(dockStoreInjectable),
|
|
||||||
}),
|
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.singleton,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default searchStoreInjectable;
|
|
||||||
@ -7,7 +7,6 @@ import { SearchStore } from "./search-store";
|
|||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
import searchStoreInjectable from "./search-store.injectable";
|
|
||||||
import directoryForUserDataInjectable
|
import directoryForUserDataInjectable
|
||||||
from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
|
||||||
@ -35,7 +34,7 @@ describe("search store tests", () => {
|
|||||||
|
|
||||||
await di.runSetups();
|
await di.runSetups();
|
||||||
|
|
||||||
searchStore = di.inject(searchStoreInjectable);
|
searchStore = new SearchStore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing with empty search query", () => {
|
it("does nothing with empty search query", () => {
|
||||||
|
|||||||
@ -3,14 +3,9 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
import { action, computed, observable, makeObservable } from "mobx";
|
||||||
import type { DockStore } from "../components/dock/dock-store/dock.store";
|
|
||||||
import { boundMethod } from "../utils";
|
import { boundMethod } from "../utils";
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
dockStore: DockStore
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SearchStore {
|
export class SearchStore {
|
||||||
/**
|
/**
|
||||||
* An utility methods escaping user string to safely pass it into new Regex(variable)
|
* An utility methods escaping user string to safely pass it into new Regex(variable)
|
||||||
@ -41,11 +36,8 @@ export class SearchStore {
|
|||||||
*/
|
*/
|
||||||
@observable activeOverlayIndex = -1;
|
@observable activeOverlayIndex = -1;
|
||||||
|
|
||||||
constructor(dependencies: Dependencies) {
|
constructor() {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
reaction(() => dependencies.dockStore.selectedTabId, () => {
|
|
||||||
this.reset();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -5,9 +5,14 @@
|
|||||||
|
|
||||||
// Helper for working with time updates / data-polling callbacks
|
// Helper for working with time updates / data-polling callbacks
|
||||||
|
|
||||||
type IntervalCallback = (count: number) => void;
|
export interface IntervalFn {
|
||||||
|
start(runImmediately?: boolean): void;
|
||||||
|
stop(): void;
|
||||||
|
restart(runImmediately?: boolean): void;
|
||||||
|
readonly isRunning: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function interval(timeSec = 1, callback: IntervalCallback, autoRun = false) {
|
export function interval(timeSec = 1, callback: (count: number) => void, autoRun = false): IntervalFn {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
let timer = -1;
|
let timer = -1;
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user