diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts
index b56b999b2b..15e72f0d80 100644
--- a/integration/__tests__/cluster-pages.tests.ts
+++ b/integration/__tests__/cluster-pages.tests.ts
@@ -113,13 +113,13 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
await frame.waitForSelector(".LogList .list span.active");
const showTimestampsButton = await frame.waitForSelector(
- ".LogControls .show-timestamps",
+ "[data-testid='log-controls'] .show-timestamps",
);
await showTimestampsButton.click();
const showPreviousButton = await frame.waitForSelector(
- ".LogControls .show-previous",
+ "[data-testid='log-controls'] .show-previous",
);
await showPreviousButton.click();
diff --git a/src/behaviours/pod-logs/__snapshots__/download-logs.test.tsx.snap b/src/behaviours/pod-logs/__snapshots__/download-logs.test.tsx.snap
new file mode 100644
index 0000000000..91daf9e587
--- /dev/null
+++ b/src/behaviours/pod-logs/__snapshots__/download-logs.test.tsx.snap
@@ -0,0 +1,864 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`download logs options in pod logs dock tab when opening pod logs renders 1`] = `
+
+
+
+`;
diff --git a/src/behaviours/pod-logs/download-logs.test.tsx b/src/behaviours/pod-logs/download-logs.test.tsx
new file mode 100644
index 0000000000..c750186666
--- /dev/null
+++ b/src/behaviours/pod-logs/download-logs.test.tsx
@@ -0,0 +1,269 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+
+import type { DiContainer } from "@ogre-tools/injectable";
+import type { RenderResult } from "@testing-library/react";
+import { act, waitFor } from "@testing-library/react";
+import getPodByIdInjectable from "../../renderer/components/+workloads-pods/get-pod-by-id.injectable";
+import getPodsByOwnerIdInjectable from "../../renderer/components/+workloads-pods/get-pods-by-owner-id.injectable";
+import { SearchStore } from "../../renderer/search-store/search-store";
+import searchStoreInjectable from "../../renderer/search-store/search-store.injectable";
+import openSaveFileDialogInjectable from "../../renderer/utils/save-file.injectable";
+import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import dockStoreInjectable from "../../renderer/components/dock/dock/store.injectable";
+import areLogsPresentInjectable from "../../renderer/components/dock/logs/are-logs-present.injectable";
+import type { CallForLogs } from "../../renderer/components/dock/logs/call-for-logs.injectable";
+import callForLogsInjectable from "../../renderer/components/dock/logs/call-for-logs.injectable";
+import createPodLogsTabInjectable from "../../renderer/components/dock/logs/create-pod-logs-tab.injectable";
+import getLogTabDataInjectable from "../../renderer/components/dock/logs/get-log-tab-data.injectable";
+import getLogsWithoutTimestampsInjectable from "../../renderer/components/dock/logs/get-logs-without-timestamps.injectable";
+import getLogsInjectable from "../../renderer/components/dock/logs/get-logs.injectable";
+import getRandomIdForPodLogsTabInjectable from "../../renderer/components/dock/logs/get-random-id-for-pod-logs-tab.injectable";
+import getTimestampSplitLogsInjectable from "../../renderer/components/dock/logs/get-timestamp-split-logs.injectable";
+import loadLogsInjectable from "../../renderer/components/dock/logs/load-logs.injectable";
+import reloadLogsInjectable from "../../renderer/components/dock/logs/reload-logs.injectable";
+import setLogTabDataInjectable from "../../renderer/components/dock/logs/set-log-tab-data.injectable";
+import stopLoadingLogsInjectable from "../../renderer/components/dock/logs/stop-loading-logs.injectable";
+import { dockerPod } from "../../renderer/components/dock/logs/__test__/pod.mock";
+
+describe("download logs options in pod logs dock tab", () => {
+ let rendered: RenderResult;
+ let rendererDi: DiContainer;
+ let builder: ApplicationBuilder;
+ let openSaveFileDialogMock: jest.MockedFunction<() => void>;
+ let callForLogsMock: jest.MockedFunction;
+ const logs = new Map([["timestamp", "some-logs"]]);
+
+ beforeEach(() => {
+ const selectedPod = dockerPod;
+
+ builder = getApplicationBuilder();
+
+ builder.setEnvironmentToClusterFrame();
+
+ callForLogsMock = jest.fn();
+
+ builder.beforeApplicationStart(({ rendererDi }) => {
+ rendererDi.override(callForLogsInjectable, () => callForLogsMock);
+
+ // Overriding internals of logsViewModelInjectable
+ rendererDi.override(getLogsInjectable, () => () => ["some-logs"]);
+ rendererDi.override(getLogsWithoutTimestampsInjectable, () => () => ["some-logs"]);
+ rendererDi.override(getTimestampSplitLogsInjectable, () => () => [...logs]);
+ rendererDi.override(reloadLogsInjectable, () => jest.fn());
+ rendererDi.override(getLogTabDataInjectable, () => () => ({
+ selectedPodId: selectedPod.getId(),
+ selectedContainer: selectedPod.getContainers()[0].name,
+ namespace: "default",
+ showPrevious: true,
+ showTimestamps: false,
+ }));
+ rendererDi.override(setLogTabDataInjectable, () => jest.fn());
+ rendererDi.override(loadLogsInjectable, () => jest.fn());
+ rendererDi.override(stopLoadingLogsInjectable, () => jest.fn());
+ rendererDi.override(areLogsPresentInjectable, () => jest.fn());
+ rendererDi.override(getPodByIdInjectable, () => (id) => {
+ if (id === selectedPod.getId()) {
+ return selectedPod;
+ }
+
+ return undefined;
+ });
+ rendererDi.override(getPodsByOwnerIdInjectable, () => jest.fn());
+ rendererDi.override(searchStoreInjectable, () => new SearchStore());
+
+ rendererDi.override(getRandomIdForPodLogsTabInjectable, () => jest.fn(() => "some-irrelevant-random-id"));
+
+ openSaveFileDialogMock = jest.fn();
+ rendererDi.override(openSaveFileDialogInjectable, () => openSaveFileDialogMock);
+ });
+
+ });
+
+ describe("when opening pod logs", () => {
+ beforeEach(async () => {
+ rendered = await builder.render();
+ rendererDi = builder.dis.rendererDi;
+
+ const pod = dockerPod;
+ const createLogsTab = rendererDi.inject(createPodLogsTabInjectable);
+ const container = {
+ name: "docker-exporter",
+ image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
+ imagePullPolicy: "pull",
+ };
+
+ const dockStore = rendererDi.inject(dockStoreInjectable);
+
+ dockStore.closeTab("terminal");
+
+ createLogsTab({
+ selectedPod: pod,
+ selectedContainer: container,
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("contains download dropdown button", () => {
+ expect(rendered.getByTestId("download-logs-dropdown")).toBeInTheDocument();
+ });
+
+ describe("when clicking on button", () => {
+ beforeEach(() => {
+ const button = rendered.getByTestId("download-logs-dropdown");
+
+ act(() => button.click());
+ });
+
+ it("shows download visible logs menu item", () => {
+ expect(rendered.getByTestId("download-visible-logs")).toBeInTheDocument();
+ });
+
+ it("shows download all logs menu item", () => {
+ expect(rendered.getByTestId("download-all-logs")).toBeInTheDocument();
+ });
+
+ describe("when call for logs resolves with logs", () => {
+ beforeEach(() => {
+ callForLogsMock.mockResolvedValue("all-logs");
+ });
+
+ describe("when selected 'download visible logs'", () => {
+ beforeEach(() => {
+ const button = rendered.getByTestId("download-visible-logs");
+
+ button.click();
+ });
+
+ it("shows save dialog with proper attributes", () => {
+ expect(openSaveFileDialogMock).toHaveBeenCalledWith("dockerExporter.log", "some-logs", "text/plain");
+ });
+ });
+
+ describe("when selected 'download all logs'", () => {
+ beforeEach(async () => {
+ await act(async () => {
+ const button = rendered.getByTestId("download-all-logs");
+
+ button.click();
+ });
+ });
+
+ it("logs have been called with query", () => {
+ expect(callForLogsMock).toHaveBeenCalledWith(
+ { name: "dockerExporter", namespace: "default" },
+ { "previous": true, "timestamps": false },
+ );
+ });
+
+ it("shows save dialog with proper attributes", async () => {
+ expect(openSaveFileDialogMock).toHaveBeenCalledWith("dockerExporter.log", "all-logs", "text/plain");
+ });
+
+ it("doesn't block download dropdown for interaction after click", async () => {
+ expect(rendered.getByTestId("download-logs-dropdown")).not.toHaveAttribute("disabled");
+ });
+ });
+
+ describe("blocking user interaction after menu item click", () => {
+ it("block download dropdown for interaction when selected 'download all logs'", async () => {
+ const downloadMenuItem = rendered.getByTestId("download-all-logs");
+
+ act(() => downloadMenuItem.click());
+
+ await waitFor(() => {
+ expect(rendered.getByTestId("download-logs-dropdown")).toHaveAttribute("disabled");
+ });
+ });
+
+ it("doesn't block dropdown for interaction when selected 'download visible logs'", () => {
+ const downloadMenuItem = rendered.getByTestId("download-visible-logs");
+
+ act(() => downloadMenuItem.click());
+
+ expect(rendered.getByTestId("download-logs-dropdown")).not.toHaveAttribute("disabled");
+ });
+ });
+ });
+
+ describe("when call for logs resolves with no logs", () => {
+ beforeEach(() => {
+ callForLogsMock.mockResolvedValue("");
+ });
+
+ describe("when selected 'download visible logs'", () => {
+ beforeEach(() => {
+ const button = rendered.getByTestId("download-visible-logs");
+
+ button.click();
+ });
+
+ it("shows save dialog with proper attributes", () => {
+ expect(openSaveFileDialogMock).toHaveBeenCalledWith("dockerExporter.log", "some-logs", "text/plain");
+ });
+ });
+
+ describe("when selected 'download all logs'", () => {
+ beforeEach(async () => {
+ await act(async () => {
+ const button = rendered.getByTestId("download-all-logs");
+
+ button.click();
+ });
+ });
+
+ it("doesn't show save dialog", async () => {
+ expect(openSaveFileDialogMock).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe("when call for logs rejects", () => {
+ beforeEach(() => {
+ callForLogsMock.mockRejectedValue("error");
+ });
+
+ describe("when selected 'download visible logs'", () => {
+ beforeEach(async () => {
+ await act(async () => {
+ const button = rendered.getByTestId("download-visible-logs");
+
+ button.click();
+ });
+ });
+
+ it("shows save dialog with proper attributes", () => {
+ expect(openSaveFileDialogMock).toHaveBeenCalledWith("dockerExporter.log", "some-logs", "text/plain");
+ });
+ });
+
+ describe("when selected 'download all logs'", () => {
+ beforeEach(async () => {
+ await act(async () => {
+ const button = rendered.getByTestId("download-all-logs");
+
+ button.click();
+ });
+ });
+
+ it("logs have been called", () => {
+ expect(callForLogsMock).toHaveBeenCalledWith(
+ { name: "dockerExporter", namespace: "default" },
+ { "previous": true, "timestamps": false },
+ );
+ });
+
+ it("doesn't show save dialog", async () => {
+ expect(openSaveFileDialogMock).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx b/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx
index 744be3fae9..66874bfd9f 100644
--- a/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx
+++ b/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx
@@ -57,6 +57,8 @@ function mockLogTabViewModel(tabId: TabId, deps: Partial void;
-}
-
-const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependencies & LogControlsProps) => {
+export const LogControls = observer(({ model }: LogControlsProps) => {
const tabData = model.logTabData.get();
const pod = model.pod.get();
@@ -44,18 +37,9 @@ const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependen
model.reloadLogs();
};
- const downloadLogs = () => {
- const fileName = pod.getName();
- const logsToDownload: string[] = showTimestamps
- ? model.logs.get()
- : model.logsWithoutTimestamps.get();
-
- openSaveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
- };
-
return (
-
-
+
+
{since && (
Logs from
@@ -77,20 +61,13 @@ const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependen
onChange={togglePrevious}
className="show-previous"
/>
-
);
});
-export const LogControls = withInjectables
(NonInjectedLogControls, {
- getProps: (di, props) => ({
- openSaveFileDialog: di.inject(openSaveFileDialogInjectable),
- ...props,
- }),
-});
diff --git a/src/renderer/components/dock/logs/create-logs-tab.injectable.ts b/src/renderer/components/dock/logs/create-logs-tab.injectable.ts
index 47cef1dd95..44e3e8a0ec 100644
--- a/src/renderer/components/dock/logs/create-logs-tab.injectable.ts
+++ b/src/renderer/components/dock/logs/create-logs-tab.injectable.ts
@@ -6,20 +6,21 @@ import { getInjectable } from "@ogre-tools/injectable";
import type { DockTabCreate, DockTab, TabId } from "../dock/store";
import { TabKind } from "../dock/store";
import type { LogTabData } from "./tab-store";
-import * as uuid from "uuid";
import { runInAction } from "mobx";
import createDockTabInjectable from "../dock/create-dock-tab.injectable";
import setLogTabDataInjectable from "./set-log-tab-data.injectable";
+import getRandomIdForPodLogsTabInjectable from "./get-random-id-for-pod-logs-tab.injectable";
export type CreateLogsTabData = Pick & Omit, "owner" | "selectedPodId" | "selectedContainer" | "namespace">;
interface Dependencies {
createDockTab: (rawTabDesc: DockTabCreate, addNumber?: boolean) => DockTab;
setLogTabData: (tabId: string, data: LogTabData) => void;
+ getRandomId: () => string;
}
-const createLogsTab = ({ createDockTab, setLogTabData }: Dependencies) => (title: string, data: CreateLogsTabData): TabId => {
- const id = `log-tab-${uuid.v4()}`;
+const createLogsTab = ({ createDockTab, setLogTabData, getRandomId }: Dependencies) => (title: string, data: CreateLogsTabData): TabId => {
+ const id = `log-tab-${getRandomId()}`;
runInAction(() => {
createDockTab({
@@ -43,6 +44,7 @@ const createLogsTabInjectable = getInjectable({
instantiate: (di) => createLogsTab({
createDockTab: di.inject(createDockTabInjectable),
setLogTabData: di.inject(setLogTabDataInjectable),
+ getRandomId: di.inject(getRandomIdForPodLogsTabInjectable),
}),
});
diff --git a/src/renderer/components/dock/logs/download-all-logs.injectable.ts b/src/renderer/components/dock/logs/download-all-logs.injectable.ts
new file mode 100644
index 0000000000..abe87cc352
--- /dev/null
+++ b/src/renderer/components/dock/logs/download-all-logs.injectable.ts
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import type { PodLogsQuery } from "../../../../common/k8s-api/endpoints";
+import type { ResourceDescriptor } from "../../../../common/k8s-api/kube-api";
+import loggerInjectable from "../../../../common/logger.injectable";
+import openSaveFileDialogInjectable from "../../../utils/save-file.injectable";
+import callForLogsInjectable from "./call-for-logs.injectable";
+
+const downloadAllLogsInjectable = getInjectable({
+ id: "download-all-logs",
+
+ instantiate: (di) => {
+ const callForLogs = di.inject(callForLogsInjectable);
+ const openSaveFileDialog = di.inject(openSaveFileDialogInjectable);
+ const logger = di.inject(loggerInjectable);
+
+ return async (params: ResourceDescriptor, query: PodLogsQuery) => {
+ const logs = await callForLogs(params, query).catch(error => {
+ logger.error("Can't download logs: ", error);
+ });
+
+ if (logs) {
+ openSaveFileDialog(`${params.name}.log`, logs, "text/plain");
+ }
+ };
+ },
+});
+
+export default downloadAllLogsInjectable;
diff --git a/src/renderer/components/dock/logs/download-logs-dropdown.module.scss b/src/renderer/components/dock/logs/download-logs-dropdown.module.scss
new file mode 100644
index 0000000000..8c93ff6559
--- /dev/null
+++ b/src/renderer/components/dock/logs/download-logs-dropdown.module.scss
@@ -0,0 +1,37 @@
+.dropdown {
+ --accent-color: var(--colorInfo);
+
+ border: 1px solid var(--accent-color);
+ border-radius: 4px;
+ color: var(--accent-color);
+ display: flex;
+ align-items: center;
+ padding: calc(var(--padding) / 4) var(--padding);
+ gap: 6px;
+ position: relative;
+
+ &:disabled {
+ cursor: progress;
+ opacity: .7;
+ }
+
+ &:hover::before{
+ opacity: 0.25;
+ }
+
+ &:focus-visible {
+ box-shadow: 0 0 0 2px var(--accent-color);
+ border-color: transparent;
+ }
+
+ &::before {
+ content: " ";
+ position: absolute;
+ background: var(--accent-color);
+ width: 100%;
+ height: 100%;
+ left: 0;
+ opacity: 0;
+ transition: opacity 0.1s;
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/components/dock/logs/download-logs-dropdown.tsx b/src/renderer/components/dock/logs/download-logs-dropdown.tsx
new file mode 100644
index 0000000000..1da7b9f8a6
--- /dev/null
+++ b/src/renderer/components/dock/logs/download-logs-dropdown.tsx
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+
+import styles from "./download-logs-dropdown.module.scss";
+
+import React, { useState } from "react";
+import { Icon } from "../../icon";
+import { MenuItem } from "../../menu";
+import { Dropdown } from "../../dropdown/dropdown";
+
+interface DownloadLogsDropdownProps {
+ downloadVisibleLogs: () => void;
+ downloadAllLogs: () => Promise | undefined;
+}
+
+export function DownloadLogsDropdown({ downloadAllLogs, downloadVisibleLogs }: DownloadLogsDropdownProps) {
+ const [waiting, setWaiting] = useState(false);
+
+ const downloadAll = async () => {
+ setWaiting(true);
+
+ try {
+ await downloadAllLogs();
+ } finally {
+ setWaiting(false);
+ }
+ };
+
+ return (
+
+ Download
+
+
+ )}
+ >
+
+
+
+ );
+}
diff --git a/src/renderer/components/dock/logs/download-logs.injectable.ts b/src/renderer/components/dock/logs/download-logs.injectable.ts
new file mode 100644
index 0000000000..c4792a9b36
--- /dev/null
+++ b/src/renderer/components/dock/logs/download-logs.injectable.ts
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import openSaveFileDialogInjectable from "../../../utils/save-file.injectable";
+
+const downloadLogsInjectable = getInjectable({
+ id: "download-logs",
+
+ instantiate: (di) => {
+ const openSaveFileDialog = di.inject(openSaveFileDialogInjectable);
+
+ return (filename: string, logs: string[]) => {
+ openSaveFileDialog(filename, logs.join("\n"), "text/plain");
+ };
+ },
+});
+
+export default downloadLogsInjectable;
diff --git a/src/renderer/components/dock/logs/get-random-id-for-pod-logs-tab.injectable.ts b/src/renderer/components/dock/logs/get-random-id-for-pod-logs-tab.injectable.ts
new file mode 100644
index 0000000000..1dcadcf293
--- /dev/null
+++ b/src/renderer/components/dock/logs/get-random-id-for-pod-logs-tab.injectable.ts
@@ -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 } from "@ogre-tools/injectable";
+import getRandomIdInjectable from "../../../../common/utils/get-random-id.injectable";
+
+const getRandomIdForPodLogsTabInjectable = getInjectable({
+ id: "get-random-id-for-pod-logs-tab",
+ instantiate: (di) => di.inject(getRandomIdInjectable),
+});
+
+export default getRandomIdForPodLogsTabInjectable;
diff --git a/src/renderer/components/dock/logs/logs-view-model.injectable.ts b/src/renderer/components/dock/logs/logs-view-model.injectable.ts
index a9c027acf9..77e4ca983c 100644
--- a/src/renderer/components/dock/logs/logs-view-model.injectable.ts
+++ b/src/renderer/components/dock/logs/logs-view-model.injectable.ts
@@ -18,6 +18,8 @@ import areLogsPresentInjectable from "./are-logs-present.injectable";
import searchStoreInjectable from "../../../search-store/search-store.injectable";
import getPodsByOwnerIdInjectable from "../../+workloads-pods/get-pods-by-owner-id.injectable";
import getPodByIdInjectable from "../../+workloads-pods/get-pod-by-id.injectable";
+import downloadLogsInjectable from "./download-logs.injectable";
+import downloadAllLogsInjectable from "./download-all-logs.injectable";
export interface InstantiateArgs {
tabId: TabId;
@@ -39,6 +41,8 @@ const logsViewModelInjectable = getInjectable({
areLogsPresent: di.inject(areLogsPresentInjectable),
getPodById: di.inject(getPodByIdInjectable),
getPodsByOwnerId: di.inject(getPodsByOwnerIdInjectable),
+ downloadLogs: di.inject(downloadLogsInjectable),
+ downloadAllLogs: di.inject(downloadAllLogsInjectable),
searchStore: di.inject(searchStoreInjectable),
}),
lifecycle: lifecycleEnum.transient,
diff --git a/src/renderer/components/dock/logs/logs-view-model.ts b/src/renderer/components/dock/logs/logs-view-model.ts
index 021eb43a7a..85b8502df3 100644
--- a/src/renderer/components/dock/logs/logs-view-model.ts
+++ b/src/renderer/components/dock/logs/logs-view-model.ts
@@ -7,12 +7,13 @@ import type { IComputedValue } from "mobx";
import { computed } from "mobx";
import type { TabId } from "../dock/store";
import type { SearchStore } from "../../../search-store/search-store";
-import type { Pod } from "../../../../common/k8s-api/endpoints";
+import type { Pod, PodLogsQuery } from "../../../../common/k8s-api/endpoints";
import { isDefined } from "../../../utils";
import assert from "assert";
import type { GetPodById } from "../../+workloads-pods/get-pod-by-id.injectable";
import type { GetPodsByOwnerId } from "../../+workloads-pods/get-pods-by-owner-id.injectable";
import type { LoadLogs } from "./load-logs.injectable";
+import type { ResourceDescriptor } from "../../../../common/k8s-api/kube-api";
export interface LogTabViewModelDependencies {
getLogs: (tabId: TabId) => string[];
@@ -27,6 +28,8 @@ export interface LogTabViewModelDependencies {
getPodById: GetPodById;
getPodsByOwnerId: GetPodsByOwnerId;
areLogsPresent: (tabId: TabId) => boolean;
+ downloadLogs: (filename: string, logs: string[]) => void;
+ downloadAllLogs: (params: ResourceDescriptor, query: PodLogsQuery) => Promise;
searchStore: SearchStore;
}
@@ -77,4 +80,32 @@ export class LogTabViewModel {
reloadLogs = () => this.dependencies.reloadLogs(this.tabId, this.pod, this.logTabData);
renameTab = (title: string) => this.dependencies.renameTab(this.tabId, title);
stopLoadingLogs = () => this.dependencies.stopLoadingLogs(this.tabId);
+
+ downloadLogs = () => {
+ const pod = this.pod.get();
+ const tabData = this.logTabData.get();
+
+ if (pod && tabData) {
+ const fileName = pod.getName();
+ const logsToDownload: string[] = tabData.showTimestamps
+ ? this.logs.get()
+ : this.logsWithoutTimestamps.get();
+
+ this.dependencies.downloadLogs(`${fileName}.log`, logsToDownload);
+ }
+ };
+
+ downloadAllLogs = () => {
+ const pod = this.pod.get();
+ const tabData = this.logTabData.get();
+
+ if (pod && tabData) {
+ const params = { name: pod.getName(), namespace: pod.getNs() };
+ const query = { timestamps: tabData.showTimestamps, previous: tabData.showPrevious };
+
+ return this.dependencies.downloadAllLogs(params, query);
+ }
+
+ return;
+ };
}
diff --git a/src/renderer/components/dropdown/dropdown.tsx b/src/renderer/components/dropdown/dropdown.tsx
new file mode 100644
index 0000000000..40efd877c6
--- /dev/null
+++ b/src/renderer/components/dropdown/dropdown.tsx
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+
+import type { HTMLAttributes } from "react";
+import React, { useState } from "react";
+import { Menu } from "../menu";
+
+interface DropdownProps extends HTMLAttributes {
+ contentForToggle: React.ReactNode;
+}
+
+export function Dropdown(props: DropdownProps) {
+ const { id, contentForToggle, children, ...rest } = props;
+ const [opened, setOpened] = useState(false);
+
+ const toggle = () => {
+ setOpened(!opened);
+ };
+
+ return (
+
+
+ {contentForToggle}
+
+
+
+ );
+}