mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into wrap-pod-logs
This commit is contained in:
commit
e02278d515
73 files changed
+2590
-661
No files matched your search
@@ -0,0 +1,865 @@
|
||||
// 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"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<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>
|
||||
`;
|
||||
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
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 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 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.beforeWindowStart((windowDi) => {
|
||||
windowDi.override(callForLogsInjectable, () => callForLogsMock);
|
||||
|
||||
// Overriding internals of logsViewModelInjectable
|
||||
windowDi.override(getLogsInjectable, () => () => ["some-logs"]);
|
||||
windowDi.override(getLogsWithoutTimestampsInjectable, () => () => ["some-logs"]);
|
||||
windowDi.override(getTimestampSplitLogsInjectable, () => () => [...logs]);
|
||||
windowDi.override(reloadLogsInjectable, () => jest.fn());
|
||||
windowDi.override(getLogTabDataInjectable, () => () => ({
|
||||
selectedPodId: selectedPod.getId(),
|
||||
selectedContainer: selectedPod.getContainers()[0].name,
|
||||
namespace: "default",
|
||||
showPrevious: true,
|
||||
showTimestamps: false,
|
||||
}));
|
||||
windowDi.override(setLogTabDataInjectable, () => jest.fn());
|
||||
windowDi.override(loadLogsInjectable, () => jest.fn());
|
||||
windowDi.override(stopLoadingLogsInjectable, () => jest.fn());
|
||||
windowDi.override(areLogsPresentInjectable, () => jest.fn());
|
||||
windowDi.override(getPodByIdInjectable, () => (id) => {
|
||||
if (id === selectedPod.getId()) {
|
||||
return selectedPod;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
windowDi.override(getPodsByOwnerIdInjectable, () => jest.fn());
|
||||
|
||||
windowDi.override(getRandomIdForPodLogsTabInjectable, () => jest.fn(() => "some-irrelevant-random-id"));
|
||||
|
||||
openSaveFileDialogMock = jest.fn();
|
||||
windowDi.override(openSaveFileDialogInjectable, () => openSaveFileDialogMock);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when opening pod logs", () => {
|
||||
beforeEach(async () => {
|
||||
rendered = await builder.render();
|
||||
|
||||
const windowDi = builder.applicationWindow.only.di;
|
||||
const pod = dockerPod;
|
||||
const createLogsTab = windowDi.inject(createPodLogsTabInjectable);
|
||||
const container = {
|
||||
name: "docker-exporter",
|
||||
image: "docker.io/prom/node-exporter:v1.0.0-rc.0",
|
||||
imagePullPolicy: "pull",
|
||||
};
|
||||
|
||||
const dockStore = windowDi.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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,8 +6,6 @@
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
|
||||
export const allowedResourcesInjectionToken = getInjectionToken<
|
||||
IComputedValue<Set<string>>
|
||||
>({
|
||||
export const allowedResourcesInjectionToken = getInjectionToken<IComputedValue<Set<string>>>({
|
||||
id: "allowed-resources",
|
||||
});
|
||||
@@ -81,7 +81,7 @@ export function sortCharts(charts: RawHelmChart[]) {
|
||||
iter.map(
|
||||
charts,
|
||||
chart => {
|
||||
const __version = coerce(chart.version, { includePrerelease: true, loose: true });
|
||||
const __version = coerce(chart.version, { loose: true });
|
||||
|
||||
if (!__version) {
|
||||
logger.warn(`[HELM-SERVICE]: Version from helm chart is not loosely coercable to semver.`, { name: chart.name, version: chart.version, repo: chart.repo });
|
||||
|
||||
@@ -27,7 +27,6 @@ export const isCompatibleExtension = ({ appSemVer }: Dependencies): ((manifest:
|
||||
|
||||
const { major: extMajor, minor: extMinor } = semver.coerce(manifestLensEngine, {
|
||||
loose: true,
|
||||
includePrerelease: false,
|
||||
}) as semver.SemVer;
|
||||
const supportedVersionsByExtension = semver.validRange(`^${extMajor}.${extMinor}`) as string;
|
||||
|
||||
|
||||
@@ -3,11 +3,17 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import themeStoreInjectable from "../../renderer/themes/store.injectable";
|
||||
import activeThemeInjectable from "../../renderer/themes/active.injectable";
|
||||
import type { LensTheme } from "../../renderer/themes/store";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
|
||||
const themeStore = asLegacyGlobalForExtensionApi(themeStoreInjectable);
|
||||
export const activeTheme = asLegacyGlobalForExtensionApi(activeThemeInjectable);
|
||||
|
||||
/**
|
||||
* @deprecated This hides the reactivity of active theme, use {@link activeTheme} instead
|
||||
*/
|
||||
export function getActiveTheme() {
|
||||
return themeStore.activeTheme;
|
||||
return activeTheme.get();
|
||||
}
|
||||
|
||||
export type { LensTheme };
|
||||
@@ -298,6 +298,7 @@ exports[`add-cluster - navigation using application menu when navigating to add
|
||||
>
|
||||
<button
|
||||
class="Button primary"
|
||||
data-waiting="false"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -350,6 +350,7 @@ exports[`cluster - order of sidebar items when rendered renders 1`] = `
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -987,6 +988,7 @@ exports[`cluster - order of sidebar items when rendered when parent is expanded
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
|
||||
@@ -319,6 +319,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -867,6 +868,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -1437,6 +1439,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -2886,6 +2889,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -3434,6 +3438,7 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
|
||||
+5
@@ -319,6 +319,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -867,6 +868,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -1455,6 +1457,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -3526,6 +3529,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -4074,6 +4078,7 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
|
||||
@@ -289,6 +289,7 @@ exports[`cluster - visibility of sidebar items given kube resource for route is
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -851,6 +852,7 @@ exports[`cluster - visibility of sidebar items given kube resource for route is
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
|
||||
@@ -0,0 +1,576 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`workload overview when navigating to workload overview 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
|
||||
class="Tab flex gaps align-center"
|
||||
data-is-active-test="false"
|
||||
data-testid="tab-link-for-pods"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="label"
|
||||
>
|
||||
Pods
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<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
|
||||
class="workload"
|
||||
>
|
||||
<div
|
||||
class="title"
|
||||
>
|
||||
<a>
|
||||
Pods (0)
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="OverviewWorkloadStatus"
|
||||
>
|
||||
<div
|
||||
class="flex column align-center box grow"
|
||||
>
|
||||
<div
|
||||
class="Chart PieChart flex column align-center"
|
||||
data-testid="workload-overview-status-chart-pods"
|
||||
>
|
||||
<div
|
||||
class="chart-container"
|
||||
>
|
||||
<canvas
|
||||
class="chartjs-render-monitor"
|
||||
height="0"
|
||||
style="display: block;"
|
||||
width="0"
|
||||
/>
|
||||
<div
|
||||
class="chartjs-tooltip flex column"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="legend flex wrap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="footer"
|
||||
>
|
||||
<div
|
||||
class="Dock"
|
||||
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 TerminalTab active"
|
||||
data-testid="dock-tab-for-terminal"
|
||||
id="tab-terminal"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="terminal"
|
||||
>
|
||||
terminal
|
||||
</span>
|
||||
</i>
|
||||
<div
|
||||
class="label"
|
||||
>
|
||||
<div
|
||||
class="flex align-center"
|
||||
>
|
||||
<span
|
||||
class="title"
|
||||
>
|
||||
Terminal
|
||||
</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_up"
|
||||
>
|
||||
keyboard_arrow_up
|
||||
</span>
|
||||
</i>
|
||||
<div>
|
||||
Open
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
+2
@@ -691,6 +691,7 @@ exports[`disable-cluster-pages-when-cluster-is-not-relevant given extension shou
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -1211,6 +1212,7 @@ exports[`disable-cluster-pages-when-cluster-is-not-relevant given not yet known
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
|
||||
+3
@@ -310,6 +310,7 @@ exports[`disable sidebar items when cluster is not relevant given extension shou
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -830,6 +831,7 @@ exports[`disable sidebar items when cluster is not relevant given extension shou
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -1350,6 +1352,7 @@ exports[`disable sidebar items when cluster is not relevant given not yet known
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
|
||||
+2
@@ -317,6 +317,7 @@ exports[`cluster/namespaces - edit namespaces from previously opened tab given t
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -874,6 +875,7 @@ exports[`cluster/namespaces - edit namespaces from previously opened tab given t
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { RenderResult } from "@testing-library/react";
|
||||
import navigateToWorkloadsOverviewInjectable from "../../common/front-end-routing/routes/cluster/workloads/overview/navigate-to-workloads-overview.injectable";
|
||||
import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
|
||||
|
||||
describe("workload overview", () => {
|
||||
let rendered: RenderResult;
|
||||
let applicationBuilder: ApplicationBuilder;
|
||||
|
||||
beforeEach(async () => {
|
||||
applicationBuilder = getApplicationBuilder().setEnvironmentToClusterFrame();
|
||||
applicationBuilder.allowKubeResource("pods");
|
||||
rendered = await applicationBuilder.render();
|
||||
});
|
||||
|
||||
describe("when navigating to workload overview", () => {
|
||||
beforeEach(() => {
|
||||
applicationBuilder.navigateWith(navigateToWorkloadsOverviewInjectable);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.baseElement).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows workload overview", () => {
|
||||
expect(rendered.queryByTestId("page-for-workloads-overview")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows pods pie chart", () => {
|
||||
expect(rendered.queryByTestId("workload-overview-status-chart-pods")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
+3
@@ -290,6 +290,7 @@ exports[`disable workloads overview details when cluster is not relevant given e
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -815,6 +816,7 @@ exports[`disable workloads overview details when cluster is not relevant given e
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -1335,6 +1337,7 @@ exports[`disable workloads overview details when cluster is not relevant given n
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
|
||||
@@ -306,6 +306,7 @@ exports[`extensions - navigation using application menu when navigating to exten
|
||||
<div>
|
||||
<button
|
||||
class="Button button primary"
|
||||
data-waiting="false"
|
||||
type="button"
|
||||
>
|
||||
Install
|
||||
|
||||
+1
@@ -6316,6 +6316,7 @@ exports[`add custom helm repository in preferences when navigating to preference
|
||||
<button
|
||||
class="Button waiting primary"
|
||||
data-testid="custom-helm-repository-submit-button"
|
||||
data-waiting="true"
|
||||
type="submit"
|
||||
>
|
||||
Add
|
||||
|
||||
+1
@@ -290,6 +290,7 @@ exports[`installing helm chart from new tab given tab for installing chart was n
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
|
||||
+2
@@ -290,6 +290,7 @@ exports[`installing helm chart from previously opened tab given tab for installi
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
@@ -820,6 +821,7 @@ exports[`installing helm chart from previously opened tab given tab for installi
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
|
||||
@@ -7430,6 +7430,7 @@ exports[`showing details for helm release given application is started when navi
|
||||
<button
|
||||
class="Button primary"
|
||||
data-testid="helm-release-configuration-save-button"
|
||||
data-waiting="false"
|
||||
type="button"
|
||||
>
|
||||
Save
|
||||
@@ -8668,6 +8669,7 @@ exports[`showing details for helm release given application is started when navi
|
||||
<button
|
||||
class="Button primary"
|
||||
data-testid="helm-release-configuration-save-button"
|
||||
data-waiting="false"
|
||||
type="button"
|
||||
>
|
||||
Save
|
||||
@@ -9906,6 +9908,7 @@ exports[`showing details for helm release given application is started when navi
|
||||
<button
|
||||
class="Button waiting primary"
|
||||
data-testid="helm-release-configuration-save-button"
|
||||
data-waiting="true"
|
||||
type="button"
|
||||
>
|
||||
Save
|
||||
@@ -10971,6 +10974,7 @@ exports[`showing details for helm release given application is started when navi
|
||||
<button
|
||||
class="Button primary"
|
||||
data-testid="helm-release-configuration-save-button"
|
||||
data-waiting="false"
|
||||
type="button"
|
||||
>
|
||||
Save
|
||||
@@ -12037,6 +12041,7 @@ exports[`showing details for helm release given application is started when navi
|
||||
<button
|
||||
class="Button primary"
|
||||
data-testid="helm-release-configuration-save-button"
|
||||
data-waiting="false"
|
||||
disabled=""
|
||||
type="button"
|
||||
>
|
||||
@@ -13276,6 +13281,7 @@ exports[`showing details for helm release given application is started when navi
|
||||
<button
|
||||
class="Button primary"
|
||||
data-testid="helm-release-configuration-save-button"
|
||||
data-waiting="false"
|
||||
type="button"
|
||||
>
|
||||
Save
|
||||
|
||||
@@ -155,6 +155,12 @@ export class ContextHandler implements ClusterContextHandler {
|
||||
|
||||
if (this.clusterUrl.hostname) {
|
||||
headers.Host = this.clusterUrl.hostname;
|
||||
|
||||
// fix current IPv6 inconsistency in url.Parse() and httpProxy.
|
||||
// with url.Parse the IPv6 Hostname has no Square brackets but httpProxy needs the Square brackets to work.
|
||||
if (headers.Host.includes(":")) {
|
||||
headers.Host = `[${headers.Host}]`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -67,7 +67,7 @@ export class Kubectl {
|
||||
let version: SemVer;
|
||||
|
||||
try {
|
||||
version = new SemVer(clusterVersion, { includePrerelease: false });
|
||||
version = new SemVer(clusterVersion);
|
||||
} catch {
|
||||
version = new SemVer(Kubectl.bundledKubectlVersion);
|
||||
}
|
||||
|
||||
+114
-4
@@ -3,11 +3,10 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { PrometheusLens } from "./lens";
|
||||
import type { CoreV1Api } from "@kubernetes/client-node";
|
||||
import type { PrometheusService } from "./provider-registry";
|
||||
|
||||
export class PrometheusHelm extends PrometheusLens {
|
||||
import { inspect } from "util";
|
||||
import { PrometheusProvider, type PrometheusService } from "./provider-registry";
|
||||
export class PrometheusHelm extends PrometheusProvider {
|
||||
readonly id: string = "helm";
|
||||
readonly name: string = "Helm";
|
||||
readonly rateAccuracy: string = "5m";
|
||||
@@ -16,4 +15,115 @@ export class PrometheusHelm extends PrometheusLens {
|
||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
||||
return this.getFirstNamespacedService(client, "app=prometheus,component=server,heritage=Helm");
|
||||
}
|
||||
|
||||
public getQuery(opts: Record<string, string>, queryName: string): string {
|
||||
switch(opts.category) {
|
||||
case "cluster":
|
||||
switch (queryName) {
|
||||
case "memoryUsage":
|
||||
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (component)`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`);
|
||||
case "workloadMemoryUsage":
|
||||
return `sum(container_memory_working_set_bytes{container!="POD",container!="",instance=~"${opts.nodes}"}) by (component)`;
|
||||
case "memoryRequests":
|
||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
||||
case "memoryLimits":
|
||||
return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
||||
case "memoryCapacity":
|
||||
return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
||||
case "memoryAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
||||
case "cpuUsage":
|
||||
return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`;
|
||||
case "cpuRequests":
|
||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
|
||||
case "cpuLimits":
|
||||
return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
|
||||
case "cpuCapacity":
|
||||
return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
|
||||
case "cpuAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
|
||||
case "podUsage":
|
||||
return `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`;
|
||||
case "podCapacity":
|
||||
return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`;
|
||||
case "podAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"}) by (component)`;
|
||||
case "fsSize":
|
||||
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
|
||||
case "fsUsage":
|
||||
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
|
||||
}
|
||||
break;
|
||||
case "nodes":
|
||||
switch (queryName) {
|
||||
case "memoryUsage":
|
||||
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`;
|
||||
case "workloadMemoryUsage":
|
||||
return `sum(container_memory_working_set_bytes{container!="POD",container!=""}) by (instance)`;
|
||||
case "memoryCapacity":
|
||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||
case "memoryAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
|
||||
case "cpuUsage":
|
||||
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`;
|
||||
case "cpuCapacity":
|
||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||
case "cpuAllocatableCapacity":
|
||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||
case "fsSize":
|
||||
return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`;
|
||||
case "fsUsage":
|
||||
return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`;
|
||||
}
|
||||
break;
|
||||
case "pods":
|
||||
switch (queryName) {
|
||||
case "cpuUsage":
|
||||
return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
|
||||
case "cpuRequests":
|
||||
return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||
case "cpuLimits":
|
||||
return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||
case "memoryUsage":
|
||||
return `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||
case "memoryRequests":
|
||||
return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||
case "memoryLimits":
|
||||
return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||
case "fsUsage":
|
||||
return `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||
case "fsWrites":
|
||||
return `sum(rate(container_fs_writes_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
|
||||
case "fsReads":
|
||||
return `sum(rate(container_fs_reads_bytes_total{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
|
||||
case "networkReceive":
|
||||
return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
|
||||
case "networkTransmit":
|
||||
return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
|
||||
}
|
||||
break;
|
||||
case "pvc":
|
||||
switch (queryName) {
|
||||
case "diskUsage":
|
||||
return `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`;
|
||||
case "diskCapacity":
|
||||
return `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`;
|
||||
}
|
||||
break;
|
||||
case "ingress":
|
||||
switch (queryName) {
|
||||
case "bytesSentSuccess":
|
||||
return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*");
|
||||
case "bytesSentFailure":
|
||||
return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*");
|
||||
case "requestDurationSeconds":
|
||||
return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
|
||||
case "responseDurationSeconds":
|
||||
return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
||||
readonly isConfigurable: boolean = true;
|
||||
|
||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService> {
|
||||
return this.getFirstNamespacedService(client, "operated-prometheus=true", "self-monitor=true");
|
||||
return this.getFirstNamespacedService(client, "operated-prometheus=true");
|
||||
}
|
||||
|
||||
public getQuery(opts: Record<string, string>, queryName: string): string {
|
||||
|
||||
@@ -7,6 +7,7 @@ import styles from "./cluster-issues.module.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { computed, makeObservable } from "mobx";
|
||||
import { Icon } from "../icon";
|
||||
import { SubHeader } from "../layout/sub-header";
|
||||
@@ -14,11 +15,9 @@ import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { cssNames, prevDefault } from "../../utils";
|
||||
import type { ItemObject } from "../../../common/item.store";
|
||||
import { Spinner } from "../spinner";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import type { ApiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { KubeObjectAge } from "../kube-object/age";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import type { NodeStore } from "../+nodes/store";
|
||||
import type { EventStore } from "../+events/store";
|
||||
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
|
||||
@@ -28,6 +27,8 @@ import type { PageParam } from "../../navigation";
|
||||
import type { ToggleKubeDetailsPane } from "../kube-detail-params/toggle-details.injectable";
|
||||
import kubeSelectedUrlParamInjectable from "../kube-detail-params/kube-selected-url.injectable";
|
||||
import toggleKubeDetailsPaneInjectable from "../kube-detail-params/toggle-details.injectable";
|
||||
import type { LensTheme } from "../../themes/store";
|
||||
import activeThemeInjectable from "../../themes/active.injectable";
|
||||
|
||||
export interface ClusterIssuesProps {
|
||||
className?: string;
|
||||
@@ -48,7 +49,7 @@ enum sortBy {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
nodeStore: NodeStore;
|
||||
eventStore: EventStore;
|
||||
apiManager: ApiManager;
|
||||
@@ -166,7 +167,7 @@ class NonInjectedClusterIssues extends React.Component<ClusterIssuesProps & Depe
|
||||
sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }}
|
||||
sortSyncWithUrl={false}
|
||||
getTableRow={this.getTableRow}
|
||||
className={cssNames("box grow", this.props.themeStore.activeTheme.type)}
|
||||
className={cssNames("box grow", this.props.activeTheme.get().type)}
|
||||
>
|
||||
<TableHead nowrap>
|
||||
<TableCell className="message">Message</TableCell>
|
||||
@@ -191,7 +192,7 @@ class NonInjectedClusterIssues extends React.Component<ClusterIssuesProps & Depe
|
||||
export const ClusterIssues = withInjectables<Dependencies, ClusterIssuesProps>(NonInjectedClusterIssues, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
apiManager: di.inject(apiManagerInjectable),
|
||||
eventStore: di.inject(eventStoreInjectable),
|
||||
nodeStore: di.inject(nodeStoreInjectable),
|
||||
|
||||
@@ -16,12 +16,13 @@ import type { PieChartData } from "../chart";
|
||||
import { PieChart } from "../chart";
|
||||
import { ClusterNoMetrics } from "./cluster-no-metrics";
|
||||
import { bytesToUnits, cssNames } from "../../utils";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import type { LensTheme } from "../../themes/store";
|
||||
import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
|
||||
import nodeStoreInjectable from "../+nodes/store.injectable";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import activeThemeInjectable from "../../themes/active.injectable";
|
||||
|
||||
function createLabels(rawLabelData: [string, number | undefined][]): string[] {
|
||||
return rawLabelData.map(([key, value]) => `${key}: ${value?.toFixed(2) || "N/A"}`);
|
||||
@@ -30,13 +31,13 @@ function createLabels(rawLabelData: [string, number | undefined][]): string[] {
|
||||
interface Dependencies {
|
||||
clusterOverviewStore: ClusterOverviewStore;
|
||||
nodeStore: NodeStore;
|
||||
themeStore: ThemeStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
}
|
||||
|
||||
const NonInjectedClusterPieCharts = observer(({
|
||||
clusterOverviewStore,
|
||||
nodeStore,
|
||||
themeStore,
|
||||
activeTheme,
|
||||
}: Dependencies) => {
|
||||
const renderLimitWarning = () => {
|
||||
return (
|
||||
@@ -54,7 +55,7 @@ const NonInjectedClusterPieCharts = observer(({
|
||||
const { podUsage, podAllocatableCapacity, podCapacity } = data;
|
||||
const cpuLimitsOverload = cpuLimits > cpuAllocatableCapacity;
|
||||
const memoryLimitsOverload = memoryLimits > memoryAllocatableCapacity;
|
||||
const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor;
|
||||
const defaultColor = activeTheme.get().colors.pieChartDefaultColor;
|
||||
|
||||
if (!memoryCapacity || !cpuCapacity || !podCapacity || !memoryAllocatableCapacity || !cpuAllocatableCapacity || !podAllocatableCapacity) return null;
|
||||
const cpuData: PieChartData = {
|
||||
@@ -261,6 +262,6 @@ export const ClusterPieCharts = withInjectables<Dependencies>(NonInjectedCluster
|
||||
getProps: (di) => ({
|
||||
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
|
||||
nodeStore: di.inject(nodeStoreInjectable),
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -91,7 +91,7 @@ const attemptInstallByInfo = ({
|
||||
}
|
||||
} else {
|
||||
const versions = Object.keys(json.versions)
|
||||
.map(version => new SemVer(version, { loose: true, includePrerelease: true }))
|
||||
.map(version => new SemVer(version, { loose: true }))
|
||||
// ignore pre-releases for auto picking the version
|
||||
.filter(version => version.prerelease.length === 0);
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export class HelmChartStore extends ItemStore<HelmChart> {
|
||||
|
||||
protected sortVersions = (versions: ChartVersion[]) => {
|
||||
return versions
|
||||
.map(chartVersion => ({ ...chartVersion, __version: semver.coerce(chartVersion.version, { includePrerelease: true, loose: true }) }))
|
||||
.map(chartVersion => ({ ...chartVersion, __version: semver.coerce(chartVersion.version, { loose: true }) }))
|
||||
.sort(sortCompareChartVersions)
|
||||
.map(({ __version, ...chartVersion }) => chartVersion);
|
||||
};
|
||||
|
||||
+7
-7
@@ -3,13 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { IObservableValue } from "mobx";
|
||||
import type { IComputedValue, IObservableValue } from "mobx";
|
||||
import { runInAction, action, observable, computed } from "mobx";
|
||||
import type { TargetHelmRelease } from "../target-helm-release.injectable";
|
||||
import type { CallForHelmRelease, DetailedHelmRelease } from "./call-for-helm-release/call-for-helm-release.injectable";
|
||||
import callForHelmReleaseInjectable from "./call-for-helm-release/call-for-helm-release.injectable";
|
||||
import type { ThemeStore } from "../../../../themes/store";
|
||||
import themeStoreInjectable from "../../../../themes/store.injectable";
|
||||
import type { LensTheme } from "../../../../themes/store";
|
||||
import type { CallForHelmReleaseConfiguration } from "./call-for-helm-release-configuration/call-for-helm-release-configuration.injectable";
|
||||
import callForHelmReleaseConfigurationInjectable from "./call-for-helm-release-configuration/call-for-helm-release-configuration.injectable";
|
||||
import { toHelmRelease } from "../../releases.injectable";
|
||||
@@ -31,6 +30,7 @@ import type { NavigateToHelmReleases } from "../../../../../common/front-end-rou
|
||||
import navigateToHelmReleasesInjectable from "../../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
|
||||
import assert from "assert";
|
||||
import withOrphanPromiseInjectable from "../../../../../common/utils/with-orphan-promise/with-orphan-promise.injectable";
|
||||
import activeThemeInjectable from "../../../../themes/active.injectable";
|
||||
|
||||
const releaseDetailsModelInjectable = getInjectable({
|
||||
id: "release-details-model",
|
||||
@@ -38,7 +38,7 @@ const releaseDetailsModelInjectable = getInjectable({
|
||||
instantiate: (di, targetRelease: TargetHelmRelease) => {
|
||||
const callForHelmRelease = di.inject(callForHelmReleaseInjectable);
|
||||
const callForHelmReleaseConfiguration = di.inject(callForHelmReleaseConfigurationInjectable);
|
||||
const themeStore = di.inject(themeStoreInjectable);
|
||||
const activeTheme = di.inject(activeThemeInjectable);
|
||||
const getResourceDetailsUrl = di.inject(getResourceDetailsUrlInjectable);
|
||||
const updateRelease = di.inject(updateReleaseInjectable);
|
||||
const showCheckedErrorNotification = di.inject(showCheckedErrorNotificationInjectable);
|
||||
@@ -50,7 +50,7 @@ const releaseDetailsModelInjectable = getInjectable({
|
||||
const model = new ReleaseDetailsModel({
|
||||
callForHelmRelease,
|
||||
targetRelease,
|
||||
themeStore,
|
||||
activeTheme,
|
||||
callForHelmReleaseConfiguration,
|
||||
getResourceDetailsUrl,
|
||||
updateRelease,
|
||||
@@ -92,7 +92,7 @@ export interface ConfigurationInput {
|
||||
interface Dependencies {
|
||||
callForHelmRelease: CallForHelmRelease;
|
||||
targetRelease: TargetHelmRelease;
|
||||
themeStore: ThemeStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
callForHelmReleaseConfiguration: CallForHelmReleaseConfiguration;
|
||||
getResourceDetailsUrl: GetResourceDetailsUrl;
|
||||
updateRelease: CallForHelmReleaseUpdate;
|
||||
@@ -259,7 +259,7 @@ export class ReleaseDetailsModel {
|
||||
}
|
||||
|
||||
@computed get activeTheme() {
|
||||
return this.dependencies.themeStore.activeTheme.type;
|
||||
return this.dependencies.activeTheme.get().type;
|
||||
}
|
||||
|
||||
close = () => {
|
||||
|
||||
@@ -12,18 +12,19 @@ import { ResourceMetricsContext } from "../resource-metrics";
|
||||
import { observer } from "mobx-react";
|
||||
import { mapValues } from "lodash";
|
||||
import { type MetricsTab, metricTabOptions } from "../chart/options";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import type { LensTheme } from "../../themes/store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import activeThemeInjectable from "../../themes/active.injectable";
|
||||
|
||||
export interface NodeChartsProps {}
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
}
|
||||
|
||||
const NonInjectedNodeCharts = observer(({
|
||||
themeStore,
|
||||
activeTheme,
|
||||
}: Dependencies & NodeChartsProps) => {
|
||||
const { metrics, tab, object } = useContext(ResourceMetricsContext) ?? {};
|
||||
|
||||
@@ -31,7 +32,7 @@ const NonInjectedNodeCharts = observer(({
|
||||
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
||||
|
||||
const id = object.getId();
|
||||
const { chartCapacityColor } = themeStore.activeTheme.colors;
|
||||
const { chartCapacityColor } = activeTheme.get().colors;
|
||||
const {
|
||||
memoryUsage,
|
||||
workloadMemoryUsage,
|
||||
@@ -162,6 +163,6 @@ const NonInjectedNodeCharts = observer(({
|
||||
export const NodeCharts = withInjectables<Dependencies, NodeChartsProps>(NonInjectedNodeCharts, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -10,18 +10,19 @@ import { BarChart, memoryOptions } from "../chart";
|
||||
import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { NoMetrics } from "../resource-metrics/no-metrics";
|
||||
import { ResourceMetricsContext } from "../resource-metrics";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import type { LensTheme } from "../../themes/store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import activeThemeInjectable from "../../themes/active.injectable";
|
||||
|
||||
export interface VolumeClaimDiskChartProps {}
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
}
|
||||
|
||||
const NonInjectedVolumeClaimDiskChart = observer(({
|
||||
themeStore,
|
||||
activeTheme,
|
||||
}: Dependencies & VolumeClaimDiskChartProps) => {
|
||||
const { metrics, tab, object } = useContext(ResourceMetricsContext) ?? {};
|
||||
|
||||
@@ -29,7 +30,7 @@ const NonInjectedVolumeClaimDiskChart = observer(({
|
||||
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
||||
|
||||
const id = object.getId();
|
||||
const { chartCapacityColor } = themeStore.activeTheme.colors;
|
||||
const { chartCapacityColor } = activeTheme.get().colors;
|
||||
const { diskUsage, diskCapacity } = metrics;
|
||||
const usage = normalizeMetrics(diskUsage).data.result[0].values;
|
||||
const capacity = normalizeMetrics(diskCapacity).data.result[0].values;
|
||||
@@ -65,6 +66,6 @@ const NonInjectedVolumeClaimDiskChart = observer(({
|
||||
export const VolumeClaimDiskChart = withInjectables<Dependencies, VolumeClaimDiskChartProps>(NonInjectedVolumeClaimDiskChart, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -31,7 +31,7 @@ const NonInjectedOverviewStatuses = observer(
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<OverviewWorkloadStatus status={workload.status.get()} />
|
||||
<OverviewWorkloadStatus workload={workload} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,92 +8,101 @@ import "./overview-workload-status.scss";
|
||||
import React from "react";
|
||||
import capitalize from "lodash/capitalize";
|
||||
import { observer } from "mobx-react";
|
||||
import type { DatasetTooltipLabel, PieChartData } from "../chart";
|
||||
import type { PieChartData } from "../chart";
|
||||
import { PieChart } from "../chart";
|
||||
import { cssVar, object } from "../../utils";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import { object } from "../../utils";
|
||||
import type { LensTheme } from "../../themes/store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import type { PascalCase } from "type-fest";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import activeThemeInjectable from "../../themes/active.injectable";
|
||||
import type { Workload } from "./workloads/workload-injection-token";
|
||||
|
||||
export type LowercaseOrPascalCase<T extends string> = Lowercase<T> | PascalCase<T>;
|
||||
|
||||
export type WorkloadStatus = Partial<Record<LowercaseOrPascalCase<keyof typeof statusBackgroundColorMapping>, number>>;
|
||||
|
||||
function toLowercase<T extends string>(src: T): Lowercase<T> {
|
||||
return src.toLowerCase() as Lowercase<T>;
|
||||
}
|
||||
|
||||
export interface OverviewWorkloadStatusProps {
|
||||
status: Partial<Record<string, number>>;
|
||||
workload: Workload;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedOverviewWorkloadStatus extends React.Component<OverviewWorkloadStatusProps & Dependencies> {
|
||||
private elem: HTMLElement | null = null;
|
||||
const statusBackgroundColorMapping = {
|
||||
"running": "colorOk",
|
||||
"scheduled": "colorOk",
|
||||
"pending": "colorWarning",
|
||||
"suspended": "colorWarning",
|
||||
"evicted": "colorError",
|
||||
"succeeded": "colorSuccess",
|
||||
"failed": "colorError",
|
||||
"terminated": "colorTerminated",
|
||||
"terminating": "colorTerminated",
|
||||
"unknown": "colorVague",
|
||||
"complete": "colorSuccess",
|
||||
} as const;
|
||||
|
||||
renderChart() {
|
||||
if (!this.elem) {
|
||||
return null;
|
||||
}
|
||||
const NonInjectedOverviewWorkloadStatus = observer((props: OverviewWorkloadStatusProps & Dependencies) => {
|
||||
const {
|
||||
workload,
|
||||
activeTheme,
|
||||
} = props;
|
||||
|
||||
const cssVars = cssVar(this.elem);
|
||||
const chartData: Required<PieChartData> = {
|
||||
labels: [],
|
||||
datasets: [],
|
||||
};
|
||||
const statusesToBeShown = object.entries(workload.status.get()).filter(([, val]) => val > 0);
|
||||
const theme = activeTheme.get();
|
||||
|
||||
const statuses = object.entries(this.props.status).filter(([, val]) => val > 0);
|
||||
const emptyDataSet = {
|
||||
data: [1],
|
||||
backgroundColor: [theme.colors.pieChartDefaultColor],
|
||||
label: "Empty",
|
||||
};
|
||||
const statusDataSet = {
|
||||
label: "Status",
|
||||
data: statusesToBeShown.map(([, value]) => value),
|
||||
backgroundColor: statusesToBeShown.map(([status]) => (
|
||||
theme.colors[statusBackgroundColorMapping[toLowercase(status)]]
|
||||
)),
|
||||
tooltipLabels: statusesToBeShown.map(([status]) => (
|
||||
(percent: string) => `${capitalize(status)}: ${percent}`
|
||||
)),
|
||||
};
|
||||
|
||||
if (statuses.length === 0) {
|
||||
chartData.datasets.push({
|
||||
data: [1],
|
||||
backgroundColor: [this.props.themeStore.activeTheme.colors.pieChartDefaultColor],
|
||||
label: "Empty",
|
||||
});
|
||||
} else {
|
||||
const data: number[] = [];
|
||||
const backgroundColor: string[] = [];
|
||||
const tooltipLabels: DatasetTooltipLabel[] = [];
|
||||
const chartData: Required<PieChartData> = {
|
||||
datasets: [statusesToBeShown.length > 0 ? statusDataSet : emptyDataSet],
|
||||
|
||||
for (const [status, value] of statuses) {
|
||||
data.push(value);
|
||||
backgroundColor.push(cssVars.get(`--workload-status-${status.toLowerCase()}`).toString());
|
||||
tooltipLabels.push(percent => `${capitalize(status)}: ${percent}`);
|
||||
chartData.labels.push(`${capitalize(status)}: ${value}`);
|
||||
}
|
||||
labels: statusesToBeShown.map(
|
||||
([status, value]) => `${capitalize(status)}: ${value}`,
|
||||
),
|
||||
};
|
||||
|
||||
chartData.datasets.push({
|
||||
data,
|
||||
backgroundColor,
|
||||
label: "Status",
|
||||
tooltipLabels,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<PieChart
|
||||
data={chartData}
|
||||
options={{
|
||||
elements: {
|
||||
arc: {
|
||||
borderWidth: 0,
|
||||
return (
|
||||
<div className="OverviewWorkloadStatus">
|
||||
<div className="flex column align-center box grow">
|
||||
<PieChart
|
||||
data={chartData}
|
||||
options={{
|
||||
elements: {
|
||||
arc: {
|
||||
borderWidth: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="OverviewWorkloadStatus" ref={e => this.elem = e}>
|
||||
<div className="flex column align-center box grow">
|
||||
{this.renderChart()}
|
||||
</div>
|
||||
}}
|
||||
data-testid={`workload-overview-status-chart-${workload.title.toLowerCase().replace(/\s+/, "-")}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const OverviewWorkloadStatus = withInjectables<Dependencies, OverviewWorkloadStatusProps>(NonInjectedOverviewWorkloadStatus, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -104,7 +104,7 @@ class NonInjectedWorkloadsOverview extends React.Component<Dependencies> {
|
||||
render() {
|
||||
return (
|
||||
<SiblingsInTabLayout>
|
||||
<div className="WorkloadsOverview flex column gaps">
|
||||
<div className="WorkloadsOverview flex column gaps" data-testid="page-for-workloads-overview">
|
||||
<div className="header flex gaps align-center">
|
||||
<h5 className="box grow">Overview</h5>
|
||||
{this.renderLoadErrors()}
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import type { WorkloadStatus } from "../overview-workload-status";
|
||||
|
||||
export interface Workload {
|
||||
resourceName: string;
|
||||
open: () => void;
|
||||
amountOfItems: IComputedValue<number>;
|
||||
status: IComputedValue<Partial<Record<string, number>>>;
|
||||
status: IComputedValue<WorkloadStatus>;
|
||||
title: string;
|
||||
orderNumber: number;
|
||||
}
|
||||
|
||||
@@ -10,27 +10,28 @@ import { BarChart } from "../chart";
|
||||
import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { NoMetrics } from "../resource-metrics/no-metrics";
|
||||
import { ResourceMetricsContext } from "../resource-metrics";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import type { LensTheme } from "../../themes/store";
|
||||
import { mapValues } from "lodash";
|
||||
import { type MetricsTab, metricTabOptions } from "../chart/options";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import activeThemeInjectable from "../../themes/active.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
|
||||
export interface ContainerChartsProps {}
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
}
|
||||
|
||||
const NonInjectedContainerCharts = observer(({
|
||||
themeStore,
|
||||
activeTheme,
|
||||
}: Dependencies & ContainerChartsProps) => {
|
||||
const { metrics, tab, object } = useContext(ResourceMetricsContext) ?? {};
|
||||
|
||||
if (!metrics || !object || !tab) return null;
|
||||
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
||||
|
||||
const { chartCapacityColor } = themeStore.activeTheme.colors;
|
||||
const { chartCapacityColor } = activeTheme.get().colors;
|
||||
const {
|
||||
cpuUsage,
|
||||
cpuRequests,
|
||||
@@ -127,6 +128,6 @@ const NonInjectedContainerCharts = observer(({
|
||||
export const ContainerCharts = withInjectables<Dependencies, ContainerChartsProps>(NonInjectedContainerCharts, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
}),
|
||||
});
|
||||
@@ -49,7 +49,11 @@ export const Button = withTooltip((props: ButtonProps) => {
|
||||
|
||||
// render as button
|
||||
return (
|
||||
<button type="button" {...btnProps}>
|
||||
<button
|
||||
type="button"
|
||||
{...btnProps}
|
||||
data-waiting={typeof waiting === "boolean" ? String(waiting) : undefined}
|
||||
>
|
||||
{label}
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@@ -13,11 +13,12 @@ import type { ChartProps } from "./chart";
|
||||
import { Chart, ChartKind } from "./chart";
|
||||
import { bytesToUnits, cssNames, isObject } from "../../utils";
|
||||
import { ZebraStripesPlugin } from "./zebra-stripes.plugin";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import type { LensTheme } from "../../themes/store";
|
||||
import { NoMetrics } from "../resource-metrics/no-metrics";
|
||||
import assert from "assert";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import activeThemeInjectable from "../../themes/active.injectable";
|
||||
|
||||
export interface BarChartProps extends ChartProps {
|
||||
name?: string;
|
||||
@@ -27,11 +28,11 @@ export interface BarChartProps extends ChartProps {
|
||||
const getBarColor: Scriptable<string> = ({ dataset }) => Color(dataset?.borderColor).alpha(0.2).string();
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
}
|
||||
|
||||
const NonInjectedBarChart = observer(({
|
||||
themeStore,
|
||||
activeTheme,
|
||||
name,
|
||||
data,
|
||||
className,
|
||||
@@ -40,7 +41,7 @@ const NonInjectedBarChart = observer(({
|
||||
options: customOptions,
|
||||
...settings
|
||||
}: Dependencies & BarChartProps) => {
|
||||
const { textColorPrimary, borderFaintColor, chartStripesColor } = themeStore.activeTheme.colors;
|
||||
const { textColorPrimary, borderFaintColor, chartStripesColor } = activeTheme.get().colors;
|
||||
const { datasets: rawDatasets = [], ...rest } = data;
|
||||
const datasets = rawDatasets
|
||||
.filter(set => set.data?.length)
|
||||
@@ -168,7 +169,7 @@ const NonInjectedBarChart = observer(({
|
||||
export const BarChart = withInjectables<Dependencies, BarChartProps>(NonInjectedBarChart, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface ChartProps {
|
||||
redraw?: boolean; // If true - recreate chart instance with no animation
|
||||
title?: string;
|
||||
className?: string;
|
||||
"data-testid"?: string;
|
||||
}
|
||||
|
||||
export enum ChartKind {
|
||||
@@ -212,25 +213,26 @@ export class Chart extends React.Component<ChartProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { width, height, showChart, title, className } = this.props;
|
||||
const { width, height, showChart, title, className, "data-testid": dataTestId } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cssNames("Chart", className)}>
|
||||
{title && <div className="chart-title">{title}</div>}
|
||||
{showChart && (
|
||||
<div className="chart-container">
|
||||
<canvas
|
||||
ref={this.canvas}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
<div className="chartjs-tooltip flex column"></div>
|
||||
</div>
|
||||
)}
|
||||
{this.renderLegend()}
|
||||
</div>
|
||||
</>
|
||||
<div
|
||||
className={cssNames("Chart", className)}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
{title && <div className="chart-title">{title}</div>}
|
||||
{showChart && (
|
||||
<div className="chart-container">
|
||||
<canvas
|
||||
ref={this.canvas}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
<div className="chartjs-tooltip flex column"></div>
|
||||
</div>
|
||||
)}
|
||||
{this.renderLegend()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,10 @@ import ChartJS from "chart.js";
|
||||
import type { ChartProps } from "./chart";
|
||||
import { Chart } from "./chart";
|
||||
import { cssNames } from "../../utils";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import type { LensTheme } from "../../themes/store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import activeThemeInjectable from "../../themes/active.injectable";
|
||||
|
||||
export interface PieChartProps extends ChartProps {
|
||||
}
|
||||
@@ -44,18 +45,18 @@ function getCutout(length: number | undefined): number {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
}
|
||||
|
||||
const NonInjectedPieChart = observer(({
|
||||
themeStore,
|
||||
activeTheme,
|
||||
data,
|
||||
className,
|
||||
options,
|
||||
showChart,
|
||||
...chartProps
|
||||
}: Dependencies & PieChartProps) => {
|
||||
const { contentColor } = themeStore.activeTheme.colors;
|
||||
const { contentColor } = activeTheme.get().colors;
|
||||
const opts: ChartOptions = {
|
||||
maintainAspectRatio: false,
|
||||
tooltips: {
|
||||
@@ -68,18 +69,11 @@ const NonInjectedPieChart = observer(({
|
||||
const total = datasetData.reduce((acc, cur) => acc + cur, 0);
|
||||
const percent = Math.round((datasetData[tooltipItem.index] as number / total) * 100);
|
||||
const percentLabel = isNaN(percent) ? "N/A" : `${percent}%`;
|
||||
const tooltipLabel = dataset.tooltipLabels?.[tooltipItem.index];
|
||||
let tooltip = `${dataset.label}: ${percentLabel}`;
|
||||
const tooltipLabelCustomizer = dataset.tooltipLabels?.[tooltipItem.index];
|
||||
|
||||
if (tooltipLabel) {
|
||||
if (typeof tooltipLabel === "function") {
|
||||
tooltip = tooltipLabel(percentLabel);
|
||||
} else {
|
||||
tooltip = tooltipLabel;
|
||||
}
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
return tooltipLabelCustomizer
|
||||
? tooltipLabelCustomizer(percentLabel)
|
||||
: `${dataset.label}: ${percentLabel}`;
|
||||
},
|
||||
},
|
||||
filter: ({ datasetIndex, index }, { datasets = [] }) => {
|
||||
@@ -120,7 +114,7 @@ const NonInjectedPieChart = observer(({
|
||||
export const PieChart = withInjectables<Dependencies, PieChartProps>(NonInjectedPieChart, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependen
|
||||
getPodsByOwnerId: jest.fn(),
|
||||
searchStore: new SearchStore(),
|
||||
areLogsPresent: jest.fn(),
|
||||
downloadLogs: jest.fn(),
|
||||
downloadAllLogs: jest.fn(),
|
||||
...deps,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependen
|
||||
getPodsByOwnerId: jest.fn(),
|
||||
areLogsPresent: jest.fn(),
|
||||
searchStore: new SearchStore(),
|
||||
downloadLogs: jest.fn(),
|
||||
downloadAllLogs: jest.fn(),
|
||||
...deps,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,9 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
.LogControls {
|
||||
background: var(--dockInfoBackground);
|
||||
padding: $padding $padding * 2;
|
||||
}
|
||||
@@ -3,27 +3,20 @@
|
||||
* 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 React from "react";
|
||||
|
||||
import { cssNames } from "../../../utils";
|
||||
import { Checkbox } from "../../checkbox";
|
||||
import { Icon } from "../../icon";
|
||||
import { DownloadLogsDropdown } from "./download-logs-dropdown";
|
||||
import type { LogTabViewModel } from "./logs-view-model";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import openSaveFileDialogInjectable from "../../../utils/save-file.injectable";
|
||||
|
||||
export interface LogControlsProps {
|
||||
model: LogTabViewModel;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
openSaveFileDialog: (filename: string, contents: BlobPart | BlobPart[], type: string) => 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 (
|
||||
<div className={cssNames("LogControls flex gaps align-center justify-space-between wrap")}>
|
||||
<div className="time-range">
|
||||
<div className={styles.controls} data-testid="log-controls">
|
||||
<div>
|
||||
{since && (
|
||||
<span>
|
||||
Logs from
|
||||
@@ -77,20 +61,13 @@ const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependen
|
||||
onChange={togglePrevious}
|
||||
className="show-previous"
|
||||
/>
|
||||
<Icon
|
||||
material="get_app"
|
||||
onClick={downloadLogs}
|
||||
tooltip="Download"
|
||||
className="download-icon"
|
||||
|
||||
<DownloadLogsDropdown
|
||||
downloadVisibleLogs={model.downloadLogs}
|
||||
downloadAllLogs={model.downloadAllLogs}
|
||||
/>
|
||||
</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 { 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<LogTabData, "owner" | "selectedPodId" | "selectedContainer" | "namespace"> & Omit<Partial<LogTabData>, "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),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 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,
|
||||
|
||||
@@ -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<void>;
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -10,13 +10,14 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { cssNames } from "../../../utils";
|
||||
import type { Terminal } from "./terminal";
|
||||
import type { TerminalStore } from "./store";
|
||||
import type { ThemeStore } from "../../../themes/store";
|
||||
import type { LensTheme } from "../../../themes/store";
|
||||
import type { DockTab, DockStore } from "../dock/store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import dockStoreInjectable from "../dock/store.injectable";
|
||||
import terminalStoreInjectable from "./store.injectable";
|
||||
import assert from "assert";
|
||||
import themeStoreInjectable from "../../../themes/store.injectable";
|
||||
import activeThemeInjectable from "../../../themes/active.injectable";
|
||||
import type { IComputedValue } from "mobx";
|
||||
|
||||
export interface TerminalWindowProps {
|
||||
tab: DockTab;
|
||||
@@ -25,7 +26,7 @@ export interface TerminalWindowProps {
|
||||
interface Dependencies {
|
||||
dockStore: DockStore;
|
||||
terminalStore: TerminalStore;
|
||||
themeStore: ThemeStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
}
|
||||
|
||||
@observer
|
||||
@@ -68,7 +69,7 @@ class NonInjectedTerminalWindow extends React.Component<TerminalWindowProps & De
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={cssNames("TerminalWindow", this.props.themeStore.activeTheme.type)}
|
||||
className={cssNames("TerminalWindow", this.props.activeTheme.get().type)}
|
||||
ref={elem => this.elem = elem}
|
||||
/>
|
||||
);
|
||||
@@ -80,7 +81,7 @@ export const TerminalWindow = withInjectables<Dependencies, TerminalWindowProps>
|
||||
...props,
|
||||
dockStore: di.inject(dockStoreInjectable),
|
||||
terminalStore: di.inject(terminalStoreInjectable),
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import "./item-list-layout.scss";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import React from "react";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { computed, makeObservable } from "mobx";
|
||||
import { Observer, observer } from "mobx-react";
|
||||
import type { ConfirmDialogParams } from "../confirm-dialog";
|
||||
@@ -20,18 +21,18 @@ import { NoItems } from "../no-items";
|
||||
import { Spinner } from "../spinner";
|
||||
import type { ItemObject } from "../../../common/item.store";
|
||||
import type { Filter, PageFiltersStore } from "./page-filters/store";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import type { LensTheme } from "../../themes/store";
|
||||
import { MenuActions } from "../menu/menu-actions";
|
||||
import { MenuItem } from "../menu";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import type { UserStore } from "../../../common/user-store";
|
||||
import type { ItemListStore } from "./list-layout";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
|
||||
import pageFiltersStoreInjectable from "./page-filters/store.injectable";
|
||||
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
|
||||
import openConfirmDialogInjectable from "../confirm-dialog/open.injectable";
|
||||
import activeThemeInjectable from "../../themes/active.injectable";
|
||||
|
||||
export interface ItemListLayoutContentProps<Item extends ItemObject, PreLoadStores extends boolean> {
|
||||
getFilters: () => Filter[];
|
||||
@@ -71,7 +72,7 @@ export interface ItemListLayoutContentProps<Item extends ItemObject, PreLoadStor
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
userStore: UserStore;
|
||||
pageFiltersStore: PageFiltersStore;
|
||||
openConfirmDialog: OpenConfirmDialog;
|
||||
@@ -291,10 +292,10 @@ class NonInjectedItemListLayoutContent<
|
||||
render() {
|
||||
const {
|
||||
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks,
|
||||
detailsItem, className, tableProps = {}, tableId, getItems, themeStore,
|
||||
detailsItem, className, tableProps = {}, tableId, getItems, activeTheme,
|
||||
} = this.props;
|
||||
const selectedItemId = detailsItem && detailsItem.getId();
|
||||
const classNames = cssNames(className, "box", "grow", themeStore.activeTheme.type);
|
||||
const classNames = cssNames(className, "box", "grow", activeTheme.get().type);
|
||||
const items = getItems();
|
||||
const selectedItems = store.pickOnlySelected(items);
|
||||
|
||||
@@ -377,7 +378,7 @@ class NonInjectedItemListLayoutContent<
|
||||
export const ItemListLayoutContent = withInjectables<Dependencies, ItemListLayoutContentProps<ItemObject, boolean>>(NonInjectedItemListLayoutContent, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
userStore: di.inject(userStoreInjectable),
|
||||
pageFiltersStore: di.inject(pageFiltersStoreInjectable),
|
||||
openConfirmDialog: di.inject(openConfirmDialogInjectable),
|
||||
|
||||
@@ -126,6 +126,7 @@ exports[`kube-object-menu given kube object when removing kube object renders 1`
|
||||
<button
|
||||
class="Button ok primary"
|
||||
data-testid="confirm"
|
||||
data-waiting="false"
|
||||
type="button"
|
||||
>
|
||||
Remove
|
||||
@@ -256,6 +257,7 @@ exports[`kube-object-menu given kube object when rerendered with different kube
|
||||
<button
|
||||
class="Button ok primary"
|
||||
data-testid="confirm"
|
||||
data-waiting="false"
|
||||
type="button"
|
||||
>
|
||||
Remove
|
||||
@@ -350,6 +352,7 @@ exports[`kube-object-menu given kube object with namespace when removing kube ob
|
||||
<button
|
||||
class="Button ok primary"
|
||||
data-testid="confirm"
|
||||
data-waiting="false"
|
||||
type="button"
|
||||
>
|
||||
Remove
|
||||
@@ -444,6 +447,7 @@ exports[`kube-object-menu given kube object without namespace when removing kube
|
||||
<button
|
||||
class="Button ok primary"
|
||||
data-testid="confirm"
|
||||
data-waiting="false"
|
||||
type="button"
|
||||
>
|
||||
Remove
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import styles from "./monaco-editor.module.scss";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { IComputedValue } from "mobx";
|
||||
import { action, computed, makeObservable, observable, reaction } from "mobx";
|
||||
import { editor, Uri } from "monaco-editor";
|
||||
import type { MonacoTheme } from "./monaco-themes";
|
||||
@@ -13,10 +14,10 @@ import { type MonacoValidator, monacoValidators } from "./monaco-validators";
|
||||
import { debounce, merge } from "lodash";
|
||||
import { autoBind, cssNames, disposer } from "../../utils";
|
||||
import type { UserStore } from "../../../common/user-store";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import type { LensTheme } from "../../themes/store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
|
||||
import activeThemeInjectable from "../../themes/active.injectable";
|
||||
|
||||
export type MonacoEditorId = string;
|
||||
|
||||
@@ -39,8 +40,8 @@ export interface MonacoEditorProps {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
userStore: UserStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
}
|
||||
|
||||
export function createMonacoUri(id: MonacoEditorId): Uri {
|
||||
@@ -83,7 +84,7 @@ class NonInjectedMonacoEditor extends React.Component<MonacoEditorProps & Depend
|
||||
}
|
||||
|
||||
@computed get theme() {
|
||||
return this.props.theme ?? this.props.themeStore.activeTheme.monacoTheme;
|
||||
return this.props.theme ?? this.props.activeTheme.get().monacoTheme;
|
||||
}
|
||||
|
||||
@computed get model(): editor.ITextModel {
|
||||
@@ -306,8 +307,8 @@ export const MonacoEditor = withInjectables<Dependencies, MonacoEditorProps, Mon
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
userStore: di.inject(userStoreInjectable),
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -19,19 +19,15 @@ export function ResourceMetricsText({ metrics }: ResourceMetricsTextProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { cpuUsage, cpuRequests, cpuLimits, memoryUsage, memoryRequests, memoryLimits } = getMetricLastPoints(metrics);
|
||||
const { cpuUsage, memoryUsage } = getMetricLastPoints(metrics);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerItem name="CPU" labelsOnly>
|
||||
{cpuUsage > 0 && <Badge label={`Usage: ${cpuUsage.toPrecision(2)}`}/>}
|
||||
{cpuRequests > 0 && <Badge label={`Requests: ${cpuRequests.toPrecision(2)}`}/>}
|
||||
{cpuLimits > 0 && <Badge label={`Limits: ${cpuLimits.toPrecision(2)}`}/>}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Memory" labelsOnly>
|
||||
{memoryUsage > 0 && <Badge label={`Usage: ${bytesToUnits(memoryUsage)}`}/>}
|
||||
{memoryRequests > 0 && <Badge label={`Requests: ${bytesToUnits(memoryRequests)}`}/>}
|
||||
{memoryLimits > 0 && <Badge label={`Limits: ${bytesToUnits(memoryLimits)}`}/>}
|
||||
</DrawerItem>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
import "./select.scss";
|
||||
|
||||
import React from "react";
|
||||
import type { ObservableSet } from "mobx";
|
||||
import type { IComputedValue, ObservableSet } from "mobx";
|
||||
import { action, computed, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import ReactSelect, { components, createFilter } from "react-select";
|
||||
import type { Props as ReactSelectProps, GroupBase, MultiValue, OptionsOrGroups, PropsValue, SingleValue } from "react-select";
|
||||
import type { ThemeStore } from "../../themes/store";
|
||||
import type { LensTheme } from "../../themes/store";
|
||||
import { autoBind, cssNames } from "../../utils";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../themes/store.injectable";
|
||||
import activeThemeInjectable from "../../themes/active.injectable";
|
||||
|
||||
const { Menu } = components;
|
||||
|
||||
@@ -80,7 +80,7 @@ const defaultFilter = createFilter({
|
||||
});
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
activeTheme: IComputedValue<LensTheme>;
|
||||
}
|
||||
|
||||
export function onMultiSelectFor<Value, Option extends SelectOption<Value>, Group extends GroupBase<Option> = GroupBase<Option>>(collection: Set<Value> | ObservableSet<Value>): SelectProps<Value, Option, true, Group>["onChange"] {
|
||||
@@ -124,7 +124,7 @@ class NonInjectedSelect<
|
||||
}
|
||||
|
||||
@computed get themeClass() {
|
||||
const themeName = this.props.themeName || this.props.themeStore.activeTheme.type;
|
||||
const themeName = this.props.themeName || this.props.activeTheme.get().type;
|
||||
|
||||
return `theme-${themeName}`;
|
||||
}
|
||||
@@ -248,7 +248,7 @@ class NonInjectedSelect<
|
||||
export const Select = withInjectables<Dependencies, SelectProps<unknown, SelectOption<unknown>, boolean>>(NonInjectedSelect, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
activeTheme: di.inject(activeThemeInjectable),
|
||||
}),
|
||||
}) as <
|
||||
Value,
|
||||
|
||||
@@ -125,7 +125,7 @@ export interface ApplicationBuilder {
|
||||
helmCharts: {
|
||||
navigate: NavigateToHelmCharts;
|
||||
};
|
||||
|
||||
navigateWith: (token: Injectable<() => void, any, void>) => void;
|
||||
select: {
|
||||
openMenu: (id: string) => { selectOption: (labelText: string) => void };
|
||||
selectOption: (menuId: string, labelText: string) => void;
|
||||
@@ -441,6 +441,13 @@ export const getApplicationBuilder = () => {
|
||||
},
|
||||
},
|
||||
|
||||
navigateWith: (token) => {
|
||||
const windowDi = builder.applicationWindow.only.di;
|
||||
const navigate = windowDi.inject(token);
|
||||
|
||||
navigate();
|
||||
},
|
||||
|
||||
setEnvironmentToClusterFrame: () => {
|
||||
environment = environments.clusterFrame;
|
||||
|
||||
|
||||
@@ -1281,6 +1281,7 @@ exports[`<ClusterFrame /> given cluster without list nodes, but with namespaces
|
||||
<main>
|
||||
<div
|
||||
class="WorkloadsOverview flex column gaps"
|
||||
data-testid="page-for-workloads-overview"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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 { computed } from "mobx";
|
||||
import themeStoreInjectable from "./store.injectable";
|
||||
|
||||
const activeThemeInjectable = getInjectable({
|
||||
id: "active-theme",
|
||||
instantiate: (di) => {
|
||||
const store = di.inject(themeStoreInjectable);
|
||||
|
||||
return computed(() => store.activeTheme);
|
||||
},
|
||||
});
|
||||
|
||||
export default activeThemeInjectable;
|
||||
@@ -3,9 +3,9 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { Theme } from "./store";
|
||||
import type { LensTheme } from "./store";
|
||||
|
||||
const lensDarkTheme: Theme = {
|
||||
const lensDarkTheme: LensTheme = {
|
||||
"name": "Dark",
|
||||
"type": "dark",
|
||||
"description": "Original Lens dark theme",
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Theme } from "./store";
|
||||
import type { LensTheme } from "./store";
|
||||
|
||||
const lensLightTheme: Theme = {
|
||||
const lensLightTheme: LensTheme = {
|
||||
"name": "Light",
|
||||
"type": "light",
|
||||
"description": "Original Lens light theme",
|
||||
|
||||
@@ -18,7 +18,7 @@ import assert from "assert";
|
||||
|
||||
export type ThemeId = string;
|
||||
|
||||
export interface Theme {
|
||||
export interface LensTheme {
|
||||
name: string;
|
||||
type: "dark" | "light";
|
||||
colors: Record<string, string>;
|
||||
@@ -39,7 +39,7 @@ interface Dependencies {
|
||||
export class ThemeStore {
|
||||
private terminalColorPrefix = "terminal";
|
||||
|
||||
#themes = observable.map<ThemeId, Theme>({
|
||||
#themes = observable.map<ThemeId, LensTheme>({
|
||||
"lens-dark": lensDarkTheme,
|
||||
"lens-light": lensLightTheme,
|
||||
});
|
||||
@@ -66,9 +66,9 @@ export class ThemeStore {
|
||||
return this.dependencies.userStore.terminalTheme;
|
||||
}
|
||||
|
||||
private readonly defaultTheme: Theme;
|
||||
private readonly defaultTheme: LensTheme;
|
||||
|
||||
@computed get activeTheme(): Theme {
|
||||
@computed get activeTheme(): LensTheme {
|
||||
return this.themes.get(this.activeThemeId) ?? this.defaultTheme;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export class ThemeStore {
|
||||
}
|
||||
|
||||
get themes() {
|
||||
return this.#themes as ReadonlyDeep<Map<string, Theme>>;
|
||||
return this.#themes as ReadonlyDeep<Map<string, LensTheme>>;
|
||||
}
|
||||
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
@@ -130,7 +130,7 @@ export class ThemeStore {
|
||||
});
|
||||
}
|
||||
|
||||
getThemeById(themeId: ThemeId): Theme | undefined {
|
||||
getThemeById(themeId: ThemeId): LensTheme | undefined {
|
||||
return this.themes.get(themeId);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user