mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Generic logs view with Pod selector (#1984)
* Adding LogTabStore Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Adding Pod selector in logs tab Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Refresh containers on pod change Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Adding <LogResourceSelector /> tests Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Adding LogTabStore tests Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Clearn getPodsByOwnerId method Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Extracting dummy pods into mock file Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Eliminating containers and initContainers from store Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Refreshing tab pods if pod amount is changed in store Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * A bit of cleaning up, fixing tests Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fix lint newline errors Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Return getPodsByOwner() method Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Rename log tab when pod changes Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
1 parent
724c6c3265
commit
a157eb03e6
25 files changed
+1359
-135
No files matched your search
@@ -18,7 +18,7 @@ export class DaemonSetStore extends KubeObjectStore<DaemonSet> {
|
||||
}
|
||||
|
||||
getChildPods(daemonSet: DaemonSet): Pod[] {
|
||||
return podsStore.getPodsByOwner(daemonSet);
|
||||
return podsStore.getPodsByOwnerId(daemonSet.getId());
|
||||
}
|
||||
|
||||
getStatuses(daemonSets?: DaemonSet[]) {
|
||||
|
||||
@@ -10,7 +10,7 @@ export class JobStore extends KubeObjectStore<Job> {
|
||||
api = jobApi;
|
||||
|
||||
getChildPods(job: Job): Pod[] {
|
||||
return podsStore.getPodsByOwner(job);
|
||||
return podsStore.getPodsByOwnerId(job.getId());
|
||||
}
|
||||
|
||||
getJobsByOwner(cronJob: CronJob) {
|
||||
|
||||
@@ -3,8 +3,8 @@ import { action, observable } from "mobx";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils";
|
||||
import { IPodMetrics, Pod, PodMetrics, podMetricsApi, podsApi } from "../../api/endpoints";
|
||||
import { WorkloadKubeObject } from "../../api/workload-kube-object";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { WorkloadKubeObject } from "../../api/workload-kube-object";
|
||||
|
||||
@autobind()
|
||||
export class PodsStore extends KubeObjectStore<Pod> {
|
||||
@@ -44,6 +44,12 @@ export class PodsStore extends KubeObjectStore<Pod> {
|
||||
});
|
||||
}
|
||||
|
||||
getPodsByOwnerId(workloadId: string): Pod[] {
|
||||
return this.items.filter(pod => {
|
||||
return pod.getOwnerRefs().find(owner => owner.uid === workloadId);
|
||||
});
|
||||
}
|
||||
|
||||
getPodsByNode(node: string) {
|
||||
if (!this.isLoaded) return [];
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export class ReplicaSetStore extends KubeObjectStore<ReplicaSet> {
|
||||
}
|
||||
|
||||
getChildPods(replicaSet: ReplicaSet) {
|
||||
return podsStore.getPodsByOwner(replicaSet);
|
||||
return podsStore.getPodsByOwnerId(replicaSet.getId());
|
||||
}
|
||||
|
||||
getStatuses(replicaSets: ReplicaSet[]) {
|
||||
|
||||
@@ -17,7 +17,7 @@ export class StatefulSetStore extends KubeObjectStore<StatefulSet> {
|
||||
}
|
||||
|
||||
getChildPods(statefulSet: StatefulSet) {
|
||||
return podsStore.getPodsByOwner(statefulSet);
|
||||
return podsStore.getPodsByOwnerId(statefulSet.getId());
|
||||
}
|
||||
|
||||
getStatuses(statefulSets: StatefulSet[]) {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { render } from "@testing-library/react";
|
||||
import selectEvent from "react-select-event";
|
||||
|
||||
import { Pod } from "../../../api/endpoints";
|
||||
import { LogResourceSelector } from "../log-resource-selector";
|
||||
import { LogTabData } from "../log-tab.store";
|
||||
import { dockerPod, deploymentPod1 } from "./pod.mock";
|
||||
|
||||
const getComponent = (tabData: LogTabData) => {
|
||||
return (
|
||||
<LogResourceSelector
|
||||
tabId="tabId"
|
||||
tabData={tabData}
|
||||
save={jest.fn()}
|
||||
reload={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 />", () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { podsStore } from "../../+workloads-pods/pods.store";
|
||||
import { Pod } from "../../../api/endpoints";
|
||||
import { dockStore } from "../dock.store";
|
||||
import { logTabStore } from "../log-tab.store";
|
||||
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
|
||||
|
||||
|
||||
podsStore.items.push(new Pod(dockerPod));
|
||||
podsStore.items.push(new Pod(deploymentPod1));
|
||||
podsStore.items.push(new Pod(deploymentPod2));
|
||||
|
||||
describe("log tab store", () => {
|
||||
afterEach(() => {
|
||||
logTabStore.reset();
|
||||
dockStore.reset();
|
||||
});
|
||||
|
||||
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({
|
||||
pods: [selectedPod],
|
||||
selectedPod,
|
||||
selectedContainer,
|
||||
showTimestamps: false,
|
||||
previous: false
|
||||
});
|
||||
});
|
||||
|
||||
it("creates log tab with sibling pods", () => {
|
||||
const selectedPod = new Pod(deploymentPod1);
|
||||
const siblingPod = new Pod(deploymentPod2);
|
||||
const selectedContainer = selectedPod.getInitContainers()[0];
|
||||
|
||||
logTabStore.createPodTab({
|
||||
selectedPod,
|
||||
selectedContainer
|
||||
});
|
||||
|
||||
expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
|
||||
pods: [selectedPod, siblingPod],
|
||||
selectedPod,
|
||||
selectedContainer,
|
||||
showTimestamps: false,
|
||||
previous: false
|
||||
});
|
||||
});
|
||||
|
||||
it("removes item from pods list if pod deleted from store", () => {
|
||||
const selectedPod = new Pod(deploymentPod1);
|
||||
const selectedContainer = selectedPod.getInitContainers()[0];
|
||||
|
||||
logTabStore.createPodTab({
|
||||
selectedPod,
|
||||
selectedContainer
|
||||
});
|
||||
|
||||
podsStore.items.pop();
|
||||
|
||||
expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
|
||||
pods: [selectedPod],
|
||||
selectedPod,
|
||||
selectedContainer,
|
||||
showTimestamps: false,
|
||||
previous: false
|
||||
});
|
||||
});
|
||||
|
||||
it("adds item into pods list if new sibling pod added to store", () => {
|
||||
const selectedPod = new Pod(deploymentPod1);
|
||||
const selectedContainer = selectedPod.getInitContainers()[0];
|
||||
|
||||
logTabStore.createPodTab({
|
||||
selectedPod,
|
||||
selectedContainer
|
||||
});
|
||||
|
||||
podsStore.items.push(new Pod(deploymentPod3));
|
||||
|
||||
expect(logTabStore.getData(dockStore.selectedTabId)).toEqual({
|
||||
pods: [selectedPod, deploymentPod3],
|
||||
selectedPod,
|
||||
selectedContainer,
|
||||
showTimestamps: false,
|
||||
previous: false
|
||||
});
|
||||
});
|
||||
|
||||
it("closes tab if no pods left in store", () => {
|
||||
const selectedPod = new Pod(deploymentPod1);
|
||||
const selectedContainer = selectedPod.getInitContainers()[0];
|
||||
|
||||
logTabStore.createPodTab({
|
||||
selectedPod,
|
||||
selectedContainer
|
||||
});
|
||||
|
||||
podsStore.items.clear();
|
||||
|
||||
expect(logTabStore.getData(dockStore.selectedTabId)).toBeUndefined();
|
||||
expect(dockStore.getTabById(dockStore.selectedTabId)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
export const dockerPod = {
|
||||
apiVersion: "v1",
|
||||
kind: "dummy",
|
||||
metadata: {
|
||||
uid: "dockerExporter",
|
||||
name: "dockerExporter",
|
||||
creationTimestamp: "dummy",
|
||||
resourceVersion: "dummy",
|
||||
namespace: "default"
|
||||
},
|
||||
spec: {
|
||||
initContainers: [] as any,
|
||||
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 deploymentPod1 = {
|
||||
apiVersion: "v1",
|
||||
kind: "dummy",
|
||||
metadata: {
|
||||
uid: "deploymentPod1",
|
||||
name: "deploymentPod1",
|
||||
creationTimestamp: "dummy",
|
||||
resourceVersion: "dummy",
|
||||
namespace: "default",
|
||||
ownerReferences: [{
|
||||
apiVersion: "v1",
|
||||
kind: "Deployment",
|
||||
name: "super-deployment",
|
||||
uid: "uuid",
|
||||
controller: true,
|
||||
blockOwnerDeletion: true,
|
||||
}]
|
||||
},
|
||||
spec: {
|
||||
initContainers: [
|
||||
{
|
||||
name: "init-node-exporter",
|
||||
image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
|
||||
imagePullPolicy: "pull"
|
||||
},
|
||||
{
|
||||
name: "init-node-exporter-1",
|
||||
image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
|
||||
imagePullPolicy: "pull"
|
||||
}
|
||||
],
|
||||
containers: [
|
||||
{
|
||||
name: "node-exporter",
|
||||
image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
|
||||
imagePullPolicy: "pull"
|
||||
},
|
||||
{
|
||||
name: "node-exporter-1",
|
||||
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 deploymentPod2 = {
|
||||
apiVersion: "v1",
|
||||
kind: "dummy",
|
||||
metadata: {
|
||||
uid: "deploymentPod2",
|
||||
name: "deploymentPod2",
|
||||
creationTimestamp: "dummy",
|
||||
resourceVersion: "dummy",
|
||||
namespace: "default",
|
||||
ownerReferences: [{
|
||||
apiVersion: "v1",
|
||||
kind: "Deployment",
|
||||
name: "super-deployment",
|
||||
uid: "uuid",
|
||||
controller: true,
|
||||
blockOwnerDeletion: true,
|
||||
}]
|
||||
},
|
||||
spec: {
|
||||
initContainers: [
|
||||
{
|
||||
name: "init-node-exporter",
|
||||
image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
|
||||
imagePullPolicy: "pull"
|
||||
},
|
||||
{
|
||||
name: "init-node-exporter-1",
|
||||
image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
|
||||
imagePullPolicy: "pull"
|
||||
}
|
||||
],
|
||||
containers: [
|
||||
{
|
||||
name: "node-exporter",
|
||||
image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
|
||||
imagePullPolicy: "pull"
|
||||
},
|
||||
{
|
||||
name: "node-exporter-1",
|
||||
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 deploymentPod3 = {
|
||||
apiVersion: "v1",
|
||||
kind: "dummy",
|
||||
metadata: {
|
||||
uid: "deploymentPod3",
|
||||
name: "deploymentPod3",
|
||||
creationTimestamp: "dummy",
|
||||
resourceVersion: "dummy",
|
||||
namespace: "default",
|
||||
ownerReferences: [{
|
||||
apiVersion: "v1",
|
||||
kind: "Deployment",
|
||||
name: "super-deployment",
|
||||
uid: "uuid",
|
||||
controller: true,
|
||||
blockOwnerDeletion: true,
|
||||
}]
|
||||
},
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: "node-exporter",
|
||||
image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
|
||||
imagePullPolicy: "pull"
|
||||
},
|
||||
{
|
||||
name: "node-exporter-1",
|
||||
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",
|
||||
}
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { DockTab } from "./dock-tab";
|
||||
import { IDockTab } from "./dock.store";
|
||||
import { isEditResourceTab } from "./edit-resource.store";
|
||||
import { isInstallChartTab } from "./install-chart.store";
|
||||
import { isLogsTab } from "./log.store";
|
||||
import { isLogsTab } from "./log-tab.store";
|
||||
import { TerminalTab } from "./terminal-tab";
|
||||
import { isTerminalTab } from "./terminal.store";
|
||||
import { isUpgradeChartTab } from "./upgrade-chart.store";
|
||||
|
||||
@@ -208,6 +208,12 @@ export class DockStore {
|
||||
this.closeTabs(tabs);
|
||||
}
|
||||
|
||||
renameTab(tabId: TabId, title: string) {
|
||||
const tab = this.getTabById(tabId);
|
||||
|
||||
tab.title = title;
|
||||
}
|
||||
|
||||
@action
|
||||
selectTab(tabId: TabId) {
|
||||
this.selectedTabId = this.getTabById(tabId)?.id ?? null;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { isEditResourceTab } from "./edit-resource.store";
|
||||
import { InstallChart } from "./install-chart";
|
||||
import { isInstallChartTab } from "./install-chart.store";
|
||||
import { Logs } from "./logs";
|
||||
import { isLogsTab } from "./log.store";
|
||||
import { isLogsTab } from "./log-tab.store";
|
||||
import { TerminalWindow } from "./terminal-window";
|
||||
import { createTerminalTab, isTerminalTab } from "./terminal.store";
|
||||
import { UpgradeChart } from "./upgrade-chart";
|
||||
|
||||
@@ -5,22 +5,23 @@ import { observer } from "mobx-react";
|
||||
|
||||
import { Pod } from "../../api/endpoints";
|
||||
import { cssNames, saveFileDialog } from "../../utils";
|
||||
import { IPodLogsData, podLogsStore } from "./log.store";
|
||||
import { logStore } from "./log.store";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { Icon } from "../icon";
|
||||
import { LogTabData } from "./log-tab.store";
|
||||
|
||||
interface Props {
|
||||
tabData: IPodLogsData
|
||||
tabData: LogTabData
|
||||
logs: string[]
|
||||
save: (data: Partial<IPodLogsData>) => void
|
||||
save: (data: Partial<LogTabData>) => void
|
||||
reload: () => void
|
||||
}
|
||||
|
||||
export const LogControls = observer((props: Props) => {
|
||||
const { tabData, save, reload, logs } = props;
|
||||
const { showTimestamps, previous } = tabData;
|
||||
const since = logs.length ? podLogsStore.getTimestamps(logs[0]) : null;
|
||||
const pod = new Pod(tabData.pod);
|
||||
const since = logs.length ? logStore.getTimestamps(logs[0]) : null;
|
||||
const pod = new Pod(tabData.selectedPod);
|
||||
|
||||
const toggleTimestamps = () => {
|
||||
save({ showTimestamps: !showTimestamps });
|
||||
@@ -33,7 +34,7 @@ export const LogControls = observer((props: Props) => {
|
||||
|
||||
const downloadLogs = () => {
|
||||
const fileName = pod.getName();
|
||||
const logsToDownload = showTimestamps ? logs : podLogsStore.logsWithoutTimestamps;
|
||||
const logsToDownload = showTimestamps ? logs : logStore.logsWithoutTimestamps;
|
||||
|
||||
saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
|
||||
};
|
||||
|
||||
@@ -14,7 +14,8 @@ import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
import { Spinner } from "../spinner";
|
||||
import { VirtualList } from "../virtual-list";
|
||||
import { podLogsStore } from "./log.store";
|
||||
import { logStore } from "./log.store";
|
||||
import { logTabStore } from "./log-tab.store";
|
||||
|
||||
interface Props {
|
||||
logs: string[]
|
||||
@@ -77,10 +78,10 @@ export class LogList extends React.Component<Props> {
|
||||
*/
|
||||
@computed
|
||||
get logs() {
|
||||
const showTimestamps = podLogsStore.getData(this.props.id).showTimestamps;
|
||||
const showTimestamps = logTabStore.getData(this.props.id).showTimestamps;
|
||||
|
||||
if (!showTimestamps) {
|
||||
return podLogsStore.logsWithoutTimestamps;
|
||||
return logStore.logsWithoutTimestamps;
|
||||
}
|
||||
|
||||
return this.props.logs;
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import "./log-resource-selector.scss";
|
||||
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { IPodContainer, Pod } from "../../api/endpoints";
|
||||
import { Pod } from "../../api/endpoints";
|
||||
import { Badge } from "../badge";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { IPodLogsData } from "./log.store";
|
||||
import { LogTabData, logTabStore } from "./log-tab.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { TabId } from "./dock.store";
|
||||
|
||||
interface Props {
|
||||
tabData: IPodLogsData
|
||||
save: (data: Partial<IPodLogsData>) => void
|
||||
tabId: TabId
|
||||
tabData: LogTabData
|
||||
save: (data: Partial<LogTabData>) => void
|
||||
reload: () => void
|
||||
}
|
||||
|
||||
export const LogResourceSelector = observer((props: Props) => {
|
||||
const { tabData, save, reload } = props;
|
||||
const { selectedContainer, containers, initContainers } = tabData;
|
||||
const pod = new Pod(tabData.pod);
|
||||
const { tabData, save, reload, tabId } = props;
|
||||
const { selectedPod, selectedContainer, pods } = tabData;
|
||||
const pod = new Pod(selectedPod);
|
||||
const containers = pod.getContainers();
|
||||
const initContainers = pod.getInitContainers();
|
||||
|
||||
const onContainerChange = (option: SelectOption) => {
|
||||
const { containers, initContainers } = tabData;
|
||||
|
||||
save({
|
||||
selectedContainer: containers
|
||||
.concat(initContainers)
|
||||
@@ -30,11 +33,18 @@ export const LogResourceSelector = observer((props: Props) => {
|
||||
reload();
|
||||
};
|
||||
|
||||
const getSelectOptions = (containers: IPodContainer[]) => {
|
||||
return containers.map(container => {
|
||||
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: container.name,
|
||||
label: container.name
|
||||
value: item,
|
||||
label: item
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -42,24 +52,43 @@ export const LogResourceSelector = observer((props: Props) => {
|
||||
const containerSelectOptions = [
|
||||
{
|
||||
label: `Containers`,
|
||||
options: getSelectOptions(containers)
|
||||
options: getSelectOptions(containers.map(container => container.name))
|
||||
},
|
||||
{
|
||||
label: `Init Containers`,
|
||||
options: getSelectOptions(initContainers),
|
||||
options: getSelectOptions(initContainers.map(container => container.name)),
|
||||
}
|
||||
];
|
||||
|
||||
const podSelectOptions = [
|
||||
{
|
||||
label: pod.getOwnerRefs()[0]?.name,
|
||||
options: getSelectOptions(pods.map(pod => pod.metadata.name))
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, [selectedPod]);
|
||||
|
||||
return (
|
||||
<div className="LogResourceSelector flex gaps align-center">
|
||||
<span>Namespace</span> <Badge label={pod.getNs()}/>
|
||||
<span>Pod</span> <Badge label={pod.getName()}/>
|
||||
<span>Namespace</span> <Badge data-testid="namespace-badge" label={pod.getNs()}/>
|
||||
<span>Pod</span>
|
||||
<Select
|
||||
options={podSelectOptions}
|
||||
value={{ label: pod.getName(), value: pod.getName() }}
|
||||
onChange={onPodChange}
|
||||
autoConvertOptions={false}
|
||||
className="pod-selector"
|
||||
/>
|
||||
<span>Container</span>
|
||||
<Select
|
||||
options={containerSelectOptions}
|
||||
value={{ label: selectedContainer.name, value: selectedContainer.name }}
|
||||
onChange={onContainerChange}
|
||||
autoConvertOptions={false}
|
||||
className="container-selector"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import { reaction } from "mobx";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
|
||||
import { IPodContainer, Pod } from "../../api/endpoints";
|
||||
import { WorkloadKubeObject } from "../../api/workload-kube-object";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { dockStore, IDockTab, TabKind } from "./dock.store";
|
||||
|
||||
export interface LogTabData {
|
||||
pods: Pod[];
|
||||
selectedPod: Pod;
|
||||
selectedContainer: IPodContainer
|
||||
showTimestamps?: boolean
|
||||
previous?: boolean
|
||||
}
|
||||
|
||||
interface PodLogsTabData {
|
||||
selectedPod: Pod
|
||||
selectedContainer: IPodContainer
|
||||
}
|
||||
|
||||
interface WorkloadLogsTabData {
|
||||
workload: WorkloadKubeObject
|
||||
}
|
||||
|
||||
export class LogTabStore extends DockTabStore<LogTabData> {
|
||||
constructor() {
|
||||
super({
|
||||
storageName: "pod_logs"
|
||||
});
|
||||
|
||||
reaction(() => podsStore.items.length, () => {
|
||||
this.updateTabsData();
|
||||
});
|
||||
}
|
||||
|
||||
createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): void {
|
||||
const podOwner = selectedPod.getOwnerRefs()[0];
|
||||
const pods = podsStore.getPodsByOwnerId(podOwner?.uid);
|
||||
const title = `Pod ${selectedPod.getName()}`;
|
||||
|
||||
this.createLogsTab(title, {
|
||||
pods: pods.length ? pods : [selectedPod],
|
||||
selectedPod,
|
||||
selectedContainer
|
||||
});
|
||||
}
|
||||
|
||||
createWorkloadTab({ workload }: WorkloadLogsTabData): void {
|
||||
const pods = podsStore.getPodsByOwnerId(workload.getId());
|
||||
|
||||
if (!pods.length) return;
|
||||
|
||||
const selectedPod = pods[0];
|
||||
const selectedContainer = selectedPod.getAllContainers()[0];
|
||||
const title = `${workload.kind} ${selectedPod.getName()}`;
|
||||
|
||||
this.createLogsTab(title, {
|
||||
pods,
|
||||
selectedPod,
|
||||
selectedContainer
|
||||
});
|
||||
}
|
||||
|
||||
renameTab(tabId: string) {
|
||||
const { selectedPod } = this.getData(tabId);
|
||||
|
||||
dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`);
|
||||
}
|
||||
|
||||
private createDockTab(tabParams: Partial<IDockTab>) {
|
||||
dockStore.createTab({
|
||||
kind: TabKind.POD_LOGS,
|
||||
...tabParams
|
||||
}, false);
|
||||
}
|
||||
|
||||
private createLogsTab(title: string, data: LogTabData) {
|
||||
const id = uniqueId("log-tab-");
|
||||
|
||||
this.createDockTab({ id, title });
|
||||
this.setData(id, {
|
||||
...data,
|
||||
showTimestamps: false,
|
||||
previous: false
|
||||
});
|
||||
}
|
||||
|
||||
private updateTabsData() {
|
||||
this.data.forEach((tabData, tabId) => {
|
||||
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) {
|
||||
this.setData(tabId, {
|
||||
...tabData,
|
||||
selectedPod,
|
||||
selectedContainer,
|
||||
pods
|
||||
});
|
||||
|
||||
this.renameTab(tabId);
|
||||
} else {
|
||||
this.closeTab(tabId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private closeTab(tabId: string) {
|
||||
this.clearData(tabId);
|
||||
dockStore.closeTab(tabId);
|
||||
}
|
||||
}
|
||||
|
||||
export const logTabStore = new LogTabStore();
|
||||
|
||||
export function isLogsTab(tab: IDockTab) {
|
||||
return tab && tab.kind === TabKind.POD_LOGS;
|
||||
}
|
||||
@@ -1,27 +1,16 @@
|
||||
import { autorun, computed, observable, reaction } from "mobx";
|
||||
import { Pod, IPodContainer, podsApi, IPodLogsQuery } from "../../api/endpoints";
|
||||
import { autorun, computed, observable } from "mobx";
|
||||
|
||||
import { IPodLogsQuery, Pod, podsApi } from "../../api/endpoints";
|
||||
import { autobind, interval } from "../../utils";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { dockStore, IDockTab, TabKind } from "./dock.store";
|
||||
import { searchStore } from "../../../common/search-store";
|
||||
import { dockStore, TabId } from "./dock.store";
|
||||
import { isLogsTab, logTabStore } from "./log-tab.store";
|
||||
|
||||
export interface IPodLogsData {
|
||||
pod: Pod;
|
||||
selectedContainer: IPodContainer
|
||||
containers: IPodContainer[]
|
||||
initContainers: IPodContainer[]
|
||||
showTimestamps: boolean
|
||||
previous: boolean
|
||||
}
|
||||
|
||||
type TabId = string;
|
||||
type PodLogLine = string;
|
||||
|
||||
// Number for log lines to load
|
||||
export const logRange = 500;
|
||||
const logLinesToLoad = 500;
|
||||
|
||||
@autobind()
|
||||
export class LogStore extends DockTabStore<IPodLogsData> {
|
||||
export class LogStore {
|
||||
private refresher = interval(10, () => {
|
||||
const id = dockStore.selectedTabId;
|
||||
|
||||
@@ -30,12 +19,8 @@ export class LogStore extends DockTabStore<IPodLogsData> {
|
||||
});
|
||||
|
||||
@observable podLogs = observable.map<TabId, PodLogLine[]>();
|
||||
@observable newLogSince = observable.map<TabId, string>(); // Timestamp after which all logs are considered to be new
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
storageName: "pod_logs"
|
||||
});
|
||||
autorun(() => {
|
||||
const { selectedTab, isOpen } = dockStore;
|
||||
|
||||
@@ -45,15 +30,6 @@ export class LogStore extends DockTabStore<IPodLogsData> {
|
||||
this.refresher.stop();
|
||||
}
|
||||
}, { delay: 500 });
|
||||
|
||||
reaction(() => this.podLogs.get(dockStore.selectedTabId), () => {
|
||||
this.setNewLogSince(dockStore.selectedTabId);
|
||||
});
|
||||
|
||||
reaction(() => dockStore.selectedTabId, () => {
|
||||
// Clear search query on tab change
|
||||
searchStore.reset();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +42,7 @@ export class LogStore extends DockTabStore<IPodLogsData> {
|
||||
load = async (tabId: TabId) => {
|
||||
try {
|
||||
const logs = await this.loadLogs(tabId, {
|
||||
tailLines: this.lines + logRange
|
||||
tailLines: this.lines + logLinesToLoad
|
||||
});
|
||||
|
||||
this.refresher.start();
|
||||
@@ -107,9 +83,9 @@ export class LogStore extends DockTabStore<IPodLogsData> {
|
||||
* @returns {Promise} A fetch request promise
|
||||
*/
|
||||
loadLogs = async (tabId: TabId, params: Partial<IPodLogsQuery>) => {
|
||||
const data = this.getData(tabId);
|
||||
const data = logTabStore.getData(tabId);
|
||||
const { selectedContainer, previous } = data;
|
||||
const pod = new Pod(data.pod);
|
||||
const pod = new Pod(data.selectedPod);
|
||||
const namespace = pod.getNs();
|
||||
const name = pod.getName();
|
||||
|
||||
@@ -127,17 +103,6 @@ export class LogStore extends DockTabStore<IPodLogsData> {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets newLogSince separator timestamp to split old logs from new ones
|
||||
* @param tabId
|
||||
*/
|
||||
setNewLogSince(tabId: TabId) {
|
||||
if (!this.podLogs.has(tabId) || !this.podLogs.get(tabId).length || this.newLogSince.has(tabId)) return;
|
||||
const timestamp = this.getLastSinceTime(tabId);
|
||||
|
||||
this.newLogSince.set(tabId, timestamp.split(".")[0]); // Removing milliseconds from string
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts logs into a string array
|
||||
* @returns {number} Length of log lines
|
||||
@@ -196,37 +161,6 @@ export class LogStore extends DockTabStore<IPodLogsData> {
|
||||
clearLogs(tabId: TabId) {
|
||||
this.podLogs.delete(tabId);
|
||||
}
|
||||
|
||||
clearData(tabId: TabId) {
|
||||
this.data.delete(tabId);
|
||||
this.clearLogs(tabId);
|
||||
}
|
||||
}
|
||||
|
||||
export const podLogsStore = new LogStore();
|
||||
|
||||
export function createPodLogsTab(data: IPodLogsData, tabParams: Partial<IDockTab> = {}) {
|
||||
const podId = data.pod.getId();
|
||||
let tab = dockStore.getTabById(podId);
|
||||
|
||||
if (tab) {
|
||||
dockStore.open();
|
||||
dockStore.selectTab(tab.id);
|
||||
|
||||
return;
|
||||
}
|
||||
// If no existent tab found
|
||||
tab = dockStore.createTab({
|
||||
id: podId,
|
||||
kind: TabKind.POD_LOGS,
|
||||
title: data.pod.getName(),
|
||||
...tabParams
|
||||
}, false);
|
||||
podLogsStore.setData(tab.id, data);
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
export function isLogsTab(tab: IDockTab) {
|
||||
return tab && tab.kind === TabKind.POD_LOGS;
|
||||
}
|
||||
export const logStore = new LogStore();
|
||||
@@ -8,9 +8,10 @@ import { IDockTab } from "./dock.store";
|
||||
import { InfoPanel } from "./info-panel";
|
||||
import { LogResourceSelector } from "./log-resource-selector";
|
||||
import { LogList } from "./log-list";
|
||||
import { IPodLogsData, podLogsStore } from "./log.store";
|
||||
import { logStore } from "./log.store";
|
||||
import { LogSearch } from "./log-search";
|
||||
import { LogControls } from "./log-controls";
|
||||
import { LogTabData, logTabStore } from "./log-tab.store";
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
@@ -30,7 +31,7 @@ export class Logs extends React.Component<Props> {
|
||||
}
|
||||
|
||||
get tabData() {
|
||||
return podLogsStore.getData(this.tabId);
|
||||
return logTabStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
@@ -38,18 +39,18 @@ export class Logs extends React.Component<Props> {
|
||||
}
|
||||
|
||||
@autobind()
|
||||
save(data: Partial<IPodLogsData>) {
|
||||
podLogsStore.setData(this.tabId, { ...this.tabData, ...data });
|
||||
save(data: Partial<LogTabData>) {
|
||||
logTabStore.setData(this.tabId, { ...this.tabData, ...data });
|
||||
}
|
||||
|
||||
load = async () => {
|
||||
this.isLoading = true;
|
||||
await podLogsStore.load(this.tabId);
|
||||
await logStore.load(this.tabId);
|
||||
this.isLoading = false;
|
||||
};
|
||||
|
||||
reload = async () => {
|
||||
podLogsStore.clearLogs(this.tabId);
|
||||
logStore.clearLogs(this.tabId);
|
||||
await this.load();
|
||||
};
|
||||
|
||||
@@ -82,11 +83,12 @@ export class Logs extends React.Component<Props> {
|
||||
}
|
||||
|
||||
renderResourceSelector() {
|
||||
const logs = podLogsStore.logs;
|
||||
const searchLogs = this.tabData.showTimestamps ? logs : podLogsStore.logsWithoutTimestamps;
|
||||
const logs = logStore.logs;
|
||||
const searchLogs = this.tabData.showTimestamps ? logs : logStore.logsWithoutTimestamps;
|
||||
const controls = (
|
||||
<div className="flex gaps">
|
||||
<LogResourceSelector
|
||||
tabId={this.tabId}
|
||||
tabData={this.tabData}
|
||||
save={this.save}
|
||||
reload={this.reload}
|
||||
@@ -112,7 +114,7 @@ export class Logs extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const logs = podLogsStore.logs;
|
||||
const logs = logStore.logs;
|
||||
|
||||
return (
|
||||
<div className="PodLogs flex column">
|
||||
|
||||
Reference in new issue
Block a user