mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add option to download all logs from the container (#5970)
* Creating callForAllLogsInjectable Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Add sketch of Download all logs button Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Use randomId while creating pod logs tab Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Initial draft of download all logs tests Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Introduce download logs dropdown Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Cleaning up Controls component Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Coloring and styling Download logs button Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Drop waiting state on network or other error Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * After clicking on button test cases Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Linter fixes Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Removing previous Download icon Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Respect timestamps and previous props in callForAllLogsInjectable Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Update snapshots Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Update snapshots Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Remove unused .mockReturnValueOnce Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Remove one more unused line Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Cleanin up by overriding internals of logsViewModel injectable Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Replace usage of callForAllLogs with simple callForLogs Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Use css modules for the Controls component Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Move downloadAllLogs logic to injectable Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Move downloadLogs logic to the model Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Remove withInjectables wrapper from LogControls Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Move downloadAllLogs to model Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Testing resolve/reject options for callForLogsInjectable Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Catching call for logs errors Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Doesn't show save dialog if no logs received Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * More descriptive describe statement Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Introduce Dropdown component with Menu Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Use <Dropdown/> in Download All Logs dropdown Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fix line-break symbol Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Update snapshots Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Return a Promise from downloadAllLogs() Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Extend LogTabViewModel mocks in other tests Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fix downloadAllLogs prop typings Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fixing linter Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fix linter harder Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Fix selectors in integration test Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> * Move tests into /behaviours Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
parent
106c3d246a
commit
5795452cc3
@ -113,13 +113,13 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
|
|||||||
await frame.waitForSelector(".LogList .list span.active");
|
await frame.waitForSelector(".LogList .list span.active");
|
||||||
|
|
||||||
const showTimestampsButton = await frame.waitForSelector(
|
const showTimestampsButton = await frame.waitForSelector(
|
||||||
".LogControls .show-timestamps",
|
"[data-testid='log-controls'] .show-timestamps",
|
||||||
);
|
);
|
||||||
|
|
||||||
await showTimestampsButton.click();
|
await showTimestampsButton.click();
|
||||||
|
|
||||||
const showPreviousButton = await frame.waitForSelector(
|
const showPreviousButton = await frame.waitForSelector(
|
||||||
".LogControls .show-previous",
|
"[data-testid='log-controls'] .show-previous",
|
||||||
);
|
);
|
||||||
|
|
||||||
await showPreviousButton.click();
|
await showPreviousButton.click();
|
||||||
|
|||||||
@ -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`] = `
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="Notifications flex column align-flex-end"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="mainLayout"
|
||||||
|
style="--sidebar-width: 200px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="sidebar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-col"
|
||||||
|
data-testid="cluster-sidebar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="SidebarCluster"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Avatar rounded loadingAvatar"
|
||||||
|
style="width: 40px; height: 40px;"
|
||||||
|
>
|
||||||
|
??
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="loadingClusterName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="sidebarNav sidebar-active-status"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="SidebarItem"
|
||||||
|
data-is-active-test="true"
|
||||||
|
data-testid="sidebar-item-workloads"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
aria-current="page"
|
||||||
|
class="nav-item flex gaps align-center expandable active"
|
||||||
|
data-testid="sidebar-item-link-for-workloads"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="Icon svg focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
/>
|
||||||
|
</i>
|
||||||
|
<span
|
||||||
|
class="link-text box grow"
|
||||||
|
>
|
||||||
|
Workloads
|
||||||
|
</span>
|
||||||
|
<i
|
||||||
|
class="Icon expand-icon box right material focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="keyboard_arrow_down"
|
||||||
|
>
|
||||||
|
keyboard_arrow_down
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="SidebarItem"
|
||||||
|
data-is-active-test="false"
|
||||||
|
data-testid="sidebar-item-config"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="nav-item flex gaps align-center"
|
||||||
|
data-testid="sidebar-item-link-for-config"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="Icon material focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="list"
|
||||||
|
>
|
||||||
|
list
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<span
|
||||||
|
class="link-text box grow"
|
||||||
|
>
|
||||||
|
Config
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="SidebarItem"
|
||||||
|
data-is-active-test="false"
|
||||||
|
data-testid="sidebar-item-network"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="nav-item flex gaps align-center expandable"
|
||||||
|
data-testid="sidebar-item-link-for-network"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="Icon material focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="device_hub"
|
||||||
|
>
|
||||||
|
device_hub
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<span
|
||||||
|
class="link-text box grow"
|
||||||
|
>
|
||||||
|
Network
|
||||||
|
</span>
|
||||||
|
<i
|
||||||
|
class="Icon expand-icon box right material focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="keyboard_arrow_down"
|
||||||
|
>
|
||||||
|
keyboard_arrow_down
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="SidebarItem"
|
||||||
|
data-is-active-test="false"
|
||||||
|
data-testid="sidebar-item-storage"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="nav-item flex gaps align-center"
|
||||||
|
data-testid="sidebar-item-link-for-storage"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="Icon material focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="storage"
|
||||||
|
>
|
||||||
|
storage
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<span
|
||||||
|
class="link-text box grow"
|
||||||
|
>
|
||||||
|
Storage
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="SidebarItem"
|
||||||
|
data-is-active-test="false"
|
||||||
|
data-testid="sidebar-item-helm"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="nav-item flex gaps align-center expandable"
|
||||||
|
data-testid="sidebar-item-link-for-helm"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="Icon svg focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
/>
|
||||||
|
</i>
|
||||||
|
<span
|
||||||
|
class="link-text box grow"
|
||||||
|
>
|
||||||
|
Helm
|
||||||
|
</span>
|
||||||
|
<i
|
||||||
|
class="Icon expand-icon box right material focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="keyboard_arrow_down"
|
||||||
|
>
|
||||||
|
keyboard_arrow_down
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="SidebarItem"
|
||||||
|
data-is-active-test="false"
|
||||||
|
data-testid="sidebar-item-user-management"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="nav-item flex gaps align-center"
|
||||||
|
data-testid="sidebar-item-link-for-user-management"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="Icon material focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="security"
|
||||||
|
>
|
||||||
|
security
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<span
|
||||||
|
class="link-text box grow"
|
||||||
|
>
|
||||||
|
Access Control
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="SidebarItem"
|
||||||
|
data-is-active-test="false"
|
||||||
|
data-testid="sidebar-item-custom-resources"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="nav-item flex gaps align-center expandable"
|
||||||
|
data-testid="sidebar-item-link-for-custom-resources"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="Icon material focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="extension"
|
||||||
|
>
|
||||||
|
extension
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<span
|
||||||
|
class="link-text box grow"
|
||||||
|
>
|
||||||
|
Custom Resources
|
||||||
|
</span>
|
||||||
|
<i
|
||||||
|
class="Icon expand-icon box right material focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="keyboard_arrow_down"
|
||||||
|
>
|
||||||
|
keyboard_arrow_down
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="ResizingAnchor horizontal trailing"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="contents"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="TabLayout"
|
||||||
|
data-testid="tab-layout"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Tabs center scrollable"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Tab flex gaps align-center active"
|
||||||
|
data-is-active-test="true"
|
||||||
|
data-testid="tab-link-for-overview"
|
||||||
|
role="tab"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
Overview
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
<div
|
||||||
|
class="WorkloadsOverview flex column gaps"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="header flex gaps align-center"
|
||||||
|
>
|
||||||
|
<h5
|
||||||
|
class="box grow"
|
||||||
|
>
|
||||||
|
Overview
|
||||||
|
</h5>
|
||||||
|
<div
|
||||||
|
class="NamespaceSelectFilterParent"
|
||||||
|
data-testid="namespace-select-filter"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Select theme-dark NamespaceSelect NamespaceSelectFilter css-b62m3t-container"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="css-1f43avz-a11yText-A11yText"
|
||||||
|
id="react-select-overview-namespace-select-filter-input-live-region"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
aria-atomic="false"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-relevant="additions text"
|
||||||
|
class="css-1f43avz-a11yText-A11yText"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="Select__control css-1s2u09g-control"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Select__value-container Select__value-container--is-multi css-319lph-ValueContainer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Select__placeholder css-14el2xx-placeholder"
|
||||||
|
id="react-select-overview-namespace-select-filter-input-placeholder"
|
||||||
|
>
|
||||||
|
All namespaces
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Select__input-container css-6j8wv5-Input"
|
||||||
|
data-value=""
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-describedby="react-select-overview-namespace-select-filter-input-placeholder"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="true"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
class="Select__input"
|
||||||
|
id="overview-namespace-select-filter-input"
|
||||||
|
role="combobox"
|
||||||
|
spellcheck="false"
|
||||||
|
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||||
|
tabindex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="css-tj5bde-Svg"
|
||||||
|
focusable="false"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="OverviewStatuses"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="workloads"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="footer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Dock isOpen"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="ResizingAnchor vertical leading"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="tabs-container flex align-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dockTabs"
|
||||||
|
role="tablist"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Tabs tabs"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Tab flex gaps align-center DockTab active"
|
||||||
|
data-testid="dock-tab-for-log-tab-some-irrelevant-random-id"
|
||||||
|
id="tab-log-tab-some-irrelevant-random-id"
|
||||||
|
role="tab"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="Icon material focusable small"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="subject"
|
||||||
|
>
|
||||||
|
subject
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<div
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex align-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="title"
|
||||||
|
>
|
||||||
|
Pod dockerExporter
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="close"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="Icon material interactive focusable small"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="close"
|
||||||
|
>
|
||||||
|
close
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<div>
|
||||||
|
Close ⌘+W
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="toolbar flex gaps align-center box grow"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dock-menu box grow"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="Icon new-dock-tab material interactive focusable"
|
||||||
|
id="menu-actions-for-dock"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="add"
|
||||||
|
>
|
||||||
|
add
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<div>
|
||||||
|
New tab
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
class="Icon material interactive focusable"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="fullscreen"
|
||||||
|
>
|
||||||
|
fullscreen
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<div>
|
||||||
|
Fit to window
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
class="Icon material interactive focusable"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="keyboard_arrow_down"
|
||||||
|
>
|
||||||
|
keyboard_arrow_down
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<div>
|
||||||
|
Minimize
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="tab-content pod-logs"
|
||||||
|
data-testid="dock-tab-content-for-log-tab-some-irrelevant-random-id"
|
||||||
|
style="flex-basis: 300px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="PodLogs flex column"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="InfoPanel flex gaps align-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="controls"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex gaps"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="LogResourceSelector flex gaps align-center"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Namespace
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="badge"
|
||||||
|
data-testid="namespace-badge"
|
||||||
|
>
|
||||||
|
default
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
Pod
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="Select theme-dark pod-selector css-b62m3t-container"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="css-1f43avz-a11yText-A11yText"
|
||||||
|
id="react-select-2-live-region"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
aria-atomic="false"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-relevant="additions text"
|
||||||
|
class="css-1f43avz-a11yText-A11yText"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="Select__control css-1s2u09g-control"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Select__single-value css-qc6sy-singleValue"
|
||||||
|
>
|
||||||
|
dockerExporter
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Select__input-container css-6j8wv5-Input"
|
||||||
|
data-value=""
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="true"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
class="Select__input"
|
||||||
|
id="react-select-2-input"
|
||||||
|
role="combobox"
|
||||||
|
spellcheck="false"
|
||||||
|
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||||
|
tabindex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="css-tj5bde-Svg"
|
||||||
|
focusable="false"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
Container
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="Select theme-dark container-selector css-b62m3t-container"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="css-1f43avz-a11yText-A11yText"
|
||||||
|
id="react-select-container-selector-input-live-region"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
aria-atomic="false"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-relevant="additions text"
|
||||||
|
class="css-1f43avz-a11yText-A11yText"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="Select__control css-1s2u09g-control"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Select__value-container Select__value-container--has-value css-319lph-ValueContainer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Select__single-value css-qc6sy-singleValue"
|
||||||
|
>
|
||||||
|
docker-exporter
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Select__input-container css-6j8wv5-Input"
|
||||||
|
data-value=""
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="true"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
class="Select__input"
|
||||||
|
id="container-selector-input"
|
||||||
|
role="combobox"
|
||||||
|
spellcheck="false"
|
||||||
|
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||||
|
tabindex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="Select__indicator-separator css-1okebmr-indicatorSeparator"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="Select__indicator Select__dropdown-indicator css-tlfecz-indicatorContainer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="css-tj5bde-Svg"
|
||||||
|
focusable="false"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="LogSearch flex box grow justify-flex-end gaps align-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Input SearchInput focused"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="input-area flex gaps align-center"
|
||||||
|
id=""
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="input box grow"
|
||||||
|
placeholder="Search..."
|
||||||
|
spellcheck="false"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
class="Icon material focusable small"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="search"
|
||||||
|
>
|
||||||
|
search
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="input-info flex gaps"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
class="Icon material interactive disabled focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="keyboard_arrow_up"
|
||||||
|
>
|
||||||
|
keyboard_arrow_up
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<div>
|
||||||
|
Previous
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
class="Icon material interactive disabled focusable"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="keyboard_arrow_down"
|
||||||
|
>
|
||||||
|
keyboard_arrow_down
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<div>
|
||||||
|
Next
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="LogList flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="VirtualList box grow"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="list"
|
||||||
|
style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="height: 18px; width: 100%;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="LogRow"
|
||||||
|
style="position: absolute; left: 0px; top: 0px; height: 18px; width: 100%;"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
some-logs
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="controls"
|
||||||
|
data-testid="log-controls"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
Logs from
|
||||||
|
|
||||||
|
<b>
|
||||||
|
Invalid Date
|
||||||
|
</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex gaps align-center"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="Checkbox flex align-center show-timestamps"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
class="box flex align-center"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
Show timestamps
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
class="Checkbox flex align-center show-previous checked"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked=""
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
class="box flex align-center"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
Show previous terminated container
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
id="download-logs-dropdown"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="dropdown"
|
||||||
|
data-testid="download-logs-dropdown"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
<i
|
||||||
|
class="Icon material focusable smallest"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="icon"
|
||||||
|
data-icon-name="arrow_drop_down"
|
||||||
|
>
|
||||||
|
arrow_drop_down
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
`;
|
||||||
269
src/behaviours/pod-logs/download-logs.test.tsx
Normal file
269
src/behaviours/pod-logs/download-logs.test.tsx
Normal file
@ -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<CallForLogs>;
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -57,6 +57,8 @@ function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependen
|
|||||||
getPodsByOwnerId: jest.fn(),
|
getPodsByOwnerId: jest.fn(),
|
||||||
searchStore: new SearchStore(),
|
searchStore: new SearchStore(),
|
||||||
areLogsPresent: jest.fn(),
|
areLogsPresent: jest.fn(),
|
||||||
|
downloadLogs: jest.fn(),
|
||||||
|
downloadAllLogs: jest.fn(),
|
||||||
...deps,
|
...deps,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,8 @@ function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependen
|
|||||||
getPodsByOwnerId: jest.fn(),
|
getPodsByOwnerId: jest.fn(),
|
||||||
areLogsPresent: jest.fn(),
|
areLogsPresent: jest.fn(),
|
||||||
searchStore: new SearchStore(),
|
searchStore: new SearchStore(),
|
||||||
|
downloadLogs: jest.fn(),
|
||||||
|
downloadAllLogs: jest.fn(),
|
||||||
...deps,
|
...deps,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/renderer/components/dock/logs/controls.module.scss
Normal file
11
src/renderer/components/dock/logs/controls.module.scss
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.controls {
|
||||||
|
@include hidden-scrollbar;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: var(--padding);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
background: var(--dockInfoBackground);
|
||||||
|
padding: var(--padding) calc(var(--padding) * 2);
|
||||||
|
}
|
||||||
@ -1,11 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.LogControls {
|
|
||||||
@include hidden-scrollbar;
|
|
||||||
|
|
||||||
background: var(--dockInfoBackground);
|
|
||||||
padding: $padding $padding * 2;
|
|
||||||
}
|
|
||||||
@ -3,27 +3,20 @@
|
|||||||
* 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 "./controls.scss";
|
import styles from "./controls.module.scss";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
import { cssNames } from "../../../utils";
|
|
||||||
import { Checkbox } from "../../checkbox";
|
import { Checkbox } from "../../checkbox";
|
||||||
import { Icon } from "../../icon";
|
import { DownloadLogsDropdown } from "./download-logs-dropdown";
|
||||||
import type { LogTabViewModel } from "./logs-view-model";
|
import type { LogTabViewModel } from "./logs-view-model";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
|
||||||
import openSaveFileDialogInjectable from "../../../utils/save-file.injectable";
|
|
||||||
|
|
||||||
export interface LogControlsProps {
|
export interface LogControlsProps {
|
||||||
model: LogTabViewModel;
|
model: LogTabViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
export const LogControls = observer(({ model }: LogControlsProps) => {
|
||||||
openSaveFileDialog: (filename: string, contents: BlobPart | BlobPart[], type: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependencies & LogControlsProps) => {
|
|
||||||
const tabData = model.logTabData.get();
|
const tabData = model.logTabData.get();
|
||||||
const pod = model.pod.get();
|
const pod = model.pod.get();
|
||||||
|
|
||||||
@ -44,18 +37,9 @@ const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependen
|
|||||||
model.reloadLogs();
|
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 (
|
return (
|
||||||
<div className={cssNames("LogControls flex gaps align-center justify-space-between wrap")}>
|
<div className={styles.controls} data-testid="log-controls">
|
||||||
<div className="time-range">
|
<div>
|
||||||
{since && (
|
{since && (
|
||||||
<span>
|
<span>
|
||||||
Logs from
|
Logs from
|
||||||
@ -77,20 +61,13 @@ const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependen
|
|||||||
onChange={togglePrevious}
|
onChange={togglePrevious}
|
||||||
className="show-previous"
|
className="show-previous"
|
||||||
/>
|
/>
|
||||||
<Icon
|
|
||||||
material="get_app"
|
<DownloadLogsDropdown
|
||||||
onClick={downloadLogs}
|
downloadVisibleLogs={model.downloadLogs}
|
||||||
tooltip="Download"
|
downloadAllLogs={model.downloadAllLogs}
|
||||||
className="download-icon"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LogControls = withInjectables<Dependencies, LogControlsProps>(NonInjectedLogControls, {
|
|
||||||
getProps: (di, props) => ({
|
|
||||||
openSaveFileDialog: di.inject(openSaveFileDialogInjectable),
|
|
||||||
...props,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|||||||
@ -6,20 +6,21 @@ import { getInjectable } from "@ogre-tools/injectable";
|
|||||||
import type { DockTabCreate, DockTab, TabId } from "../dock/store";
|
import type { DockTabCreate, DockTab, TabId } from "../dock/store";
|
||||||
import { TabKind } from "../dock/store";
|
import { TabKind } from "../dock/store";
|
||||||
import type { LogTabData } from "./tab-store";
|
import type { LogTabData } from "./tab-store";
|
||||||
import * as uuid from "uuid";
|
|
||||||
import { runInAction } from "mobx";
|
import { runInAction } from "mobx";
|
||||||
import createDockTabInjectable from "../dock/create-dock-tab.injectable";
|
import createDockTabInjectable from "../dock/create-dock-tab.injectable";
|
||||||
import setLogTabDataInjectable from "./set-log-tab-data.injectable";
|
import setLogTabDataInjectable from "./set-log-tab-data.injectable";
|
||||||
|
import getRandomIdForPodLogsTabInjectable from "./get-random-id-for-pod-logs-tab.injectable";
|
||||||
|
|
||||||
export type CreateLogsTabData = Pick<LogTabData, "owner" | "selectedPodId" | "selectedContainer" | "namespace"> & Omit<Partial<LogTabData>, "owner" | "selectedPodId" | "selectedContainer" | "namespace">;
|
export type CreateLogsTabData = Pick<LogTabData, "owner" | "selectedPodId" | "selectedContainer" | "namespace"> & Omit<Partial<LogTabData>, "owner" | "selectedPodId" | "selectedContainer" | "namespace">;
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
createDockTab: (rawTabDesc: DockTabCreate, addNumber?: boolean) => DockTab;
|
createDockTab: (rawTabDesc: DockTabCreate, addNumber?: boolean) => DockTab;
|
||||||
setLogTabData: (tabId: string, data: LogTabData) => void;
|
setLogTabData: (tabId: string, data: LogTabData) => void;
|
||||||
|
getRandomId: () => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createLogsTab = ({ createDockTab, setLogTabData }: Dependencies) => (title: string, data: CreateLogsTabData): TabId => {
|
const createLogsTab = ({ createDockTab, setLogTabData, getRandomId }: Dependencies) => (title: string, data: CreateLogsTabData): TabId => {
|
||||||
const id = `log-tab-${uuid.v4()}`;
|
const id = `log-tab-${getRandomId()}`;
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
createDockTab({
|
createDockTab({
|
||||||
@ -43,6 +44,7 @@ const createLogsTabInjectable = getInjectable({
|
|||||||
instantiate: (di) => createLogsTab({
|
instantiate: (di) => createLogsTab({
|
||||||
createDockTab: di.inject(createDockTabInjectable),
|
createDockTab: di.inject(createDockTabInjectable),
|
||||||
setLogTabData: di.inject(setLogTabDataInjectable),
|
setLogTabData: di.inject(setLogTabDataInjectable),
|
||||||
|
getRandomId: di.inject(getRandomIdForPodLogsTabInjectable),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/renderer/components/dock/logs/download-logs-dropdown.tsx
Normal file
53
src/renderer/components/dock/logs/download-logs-dropdown.tsx
Normal file
@ -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<void> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DownloadLogsDropdown({ downloadAllLogs, downloadVisibleLogs }: DownloadLogsDropdownProps) {
|
||||||
|
const [waiting, setWaiting] = useState(false);
|
||||||
|
|
||||||
|
const downloadAll = async () => {
|
||||||
|
setWaiting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadAllLogs();
|
||||||
|
} finally {
|
||||||
|
setWaiting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
id="download-logs-dropdown"
|
||||||
|
contentForToggle={(
|
||||||
|
<button
|
||||||
|
data-testid="download-logs-dropdown"
|
||||||
|
className={styles.dropdown}
|
||||||
|
disabled={waiting}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
<Icon material="arrow_drop_down" smallest/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={downloadVisibleLogs} data-testid="download-visible-logs">
|
||||||
|
Visible logs
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={downloadAll} data-testid="download-all-logs">
|
||||||
|
All logs
|
||||||
|
</MenuItem>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -18,6 +18,8 @@ import areLogsPresentInjectable from "./are-logs-present.injectable";
|
|||||||
import searchStoreInjectable from "../../../search-store/search-store.injectable";
|
import searchStoreInjectable from "../../../search-store/search-store.injectable";
|
||||||
import getPodsByOwnerIdInjectable from "../../+workloads-pods/get-pods-by-owner-id.injectable";
|
import getPodsByOwnerIdInjectable from "../../+workloads-pods/get-pods-by-owner-id.injectable";
|
||||||
import getPodByIdInjectable from "../../+workloads-pods/get-pod-by-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 {
|
export interface InstantiateArgs {
|
||||||
tabId: TabId;
|
tabId: TabId;
|
||||||
@ -39,6 +41,8 @@ const logsViewModelInjectable = getInjectable({
|
|||||||
areLogsPresent: di.inject(areLogsPresentInjectable),
|
areLogsPresent: di.inject(areLogsPresentInjectable),
|
||||||
getPodById: di.inject(getPodByIdInjectable),
|
getPodById: di.inject(getPodByIdInjectable),
|
||||||
getPodsByOwnerId: di.inject(getPodsByOwnerIdInjectable),
|
getPodsByOwnerId: di.inject(getPodsByOwnerIdInjectable),
|
||||||
|
downloadLogs: di.inject(downloadLogsInjectable),
|
||||||
|
downloadAllLogs: di.inject(downloadAllLogsInjectable),
|
||||||
searchStore: di.inject(searchStoreInjectable),
|
searchStore: di.inject(searchStoreInjectable),
|
||||||
}),
|
}),
|
||||||
lifecycle: lifecycleEnum.transient,
|
lifecycle: lifecycleEnum.transient,
|
||||||
|
|||||||
@ -7,12 +7,13 @@ import type { IComputedValue } from "mobx";
|
|||||||
import { computed } from "mobx";
|
import { computed } from "mobx";
|
||||||
import type { TabId } from "../dock/store";
|
import type { TabId } from "../dock/store";
|
||||||
import type { SearchStore } from "../../../search-store/search-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 { isDefined } from "../../../utils";
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
import type { GetPodById } from "../../+workloads-pods/get-pod-by-id.injectable";
|
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 { GetPodsByOwnerId } from "../../+workloads-pods/get-pods-by-owner-id.injectable";
|
||||||
import type { LoadLogs } from "./load-logs.injectable";
|
import type { LoadLogs } from "./load-logs.injectable";
|
||||||
|
import type { ResourceDescriptor } from "../../../../common/k8s-api/kube-api";
|
||||||
|
|
||||||
export interface LogTabViewModelDependencies {
|
export interface LogTabViewModelDependencies {
|
||||||
getLogs: (tabId: TabId) => string[];
|
getLogs: (tabId: TabId) => string[];
|
||||||
@ -27,6 +28,8 @@ export interface LogTabViewModelDependencies {
|
|||||||
getPodById: GetPodById;
|
getPodById: GetPodById;
|
||||||
getPodsByOwnerId: GetPodsByOwnerId;
|
getPodsByOwnerId: GetPodsByOwnerId;
|
||||||
areLogsPresent: (tabId: TabId) => boolean;
|
areLogsPresent: (tabId: TabId) => boolean;
|
||||||
|
downloadLogs: (filename: string, logs: string[]) => void;
|
||||||
|
downloadAllLogs: (params: ResourceDescriptor, query: PodLogsQuery) => Promise<void>;
|
||||||
searchStore: SearchStore;
|
searchStore: SearchStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,4 +80,32 @@ export class LogTabViewModel {
|
|||||||
reloadLogs = () => this.dependencies.reloadLogs(this.tabId, this.pod, this.logTabData);
|
reloadLogs = () => this.dependencies.reloadLogs(this.tabId, this.pod, this.logTabData);
|
||||||
renameTab = (title: string) => this.dependencies.renameTab(this.tabId, title);
|
renameTab = (title: string) => this.dependencies.renameTab(this.tabId, title);
|
||||||
stopLoadingLogs = () => this.dependencies.stopLoadingLogs(this.tabId);
|
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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
38
src/renderer/components/dropdown/dropdown.tsx
Normal file
38
src/renderer/components/dropdown/dropdown.tsx
Normal file
@ -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<HTMLDivElement> {
|
||||||
|
contentForToggle: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Dropdown(props: DropdownProps) {
|
||||||
|
const { id, contentForToggle, children, ...rest } = props;
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
setOpened(!opened);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...rest}>
|
||||||
|
<div id={id}>
|
||||||
|
{contentForToggle}
|
||||||
|
</div>
|
||||||
|
<Menu
|
||||||
|
usePortal
|
||||||
|
htmlFor={id}
|
||||||
|
isOpen={opened}
|
||||||
|
close={toggle}
|
||||||
|
open={toggle}
|
||||||
|
>
|
||||||
|
{React.Children.toArray(children)}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user