Restructure to monorepo (#6907)

* wip: restructure to monorepo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor create-release-pr to a package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* github workflow fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix typo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* add webpack-env types to core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix github workflows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor/fix integration tests

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* lint fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* yarn run dev

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* eslint settings for vscode

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* move templates to right package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* open-lens build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* integration test fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix nx task dependencies

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* use bash shell for unit tests in test workflow

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix test:unit for windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix build:app on windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove ELECTRON_BUILDER_EXTRA_ARGS

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* sync src/ from master

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove Makefile from core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2023-01-24 10:46:26 -08:00
1 parent 0066ca4ea2
commit 2657df2293
3321 files changed
+28699 -11447

No files matched your search

@@ -0,0 +1,488 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`extension special characters in page registrations renders 1`] = `
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="home-button"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to some-product-name!
</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
`;
exports[`extension special characters in page registrations when navigating to route with ID having special characters renders 1`] = `
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive focusable"
data-testid="home-button"
tabindex="0"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div>
Some page
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
`;
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,412 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`navigating between routes given route with optional path parameters when navigating to route with path parameters renders 1`] = `
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive focusable"
data-testid="home-button"
tabindex="0"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<pre>
{
"someParameter": "some-value",
"someOtherParameter": "some-other-value"
}
</pre>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
`;
exports[`navigating between routes given route without path parameters when navigating to route renders 1`] = `
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive focusable"
data-testid="home-button"
tabindex="0"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div>
Some component
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
`;
@@ -0,0 +1,572 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`add-cluster - navigation using application menu renders 1`] = `
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="home-button"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to some-product-name!
</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
`;
exports[`add-cluster - navigation using application menu when navigating to add cluster using application menu renders 1`] = `
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive focusable"
data-testid="home-button"
tabindex="0"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="SettingLayout AddClusters"
data-testid="add-cluster-page"
>
<div
class="contentRegion"
id="ScrollSpyRoot"
>
<div
class="content flex column gaps"
>
<h2>
Add Clusters from Kubeconfig
</h2>
<p>
Clusters added here are
<b>
not
</b>
merged into the
<code>
~/.kube/config
</code>
file.
<a
href="https://docs.k8slens.dev/getting-started/add-cluster/"
rel="noreferrer"
target="_blank"
>
Read more about adding clusters.
</a>
</p>
<div
class="flex column"
>
<textarea
data-testid="monaco-editor-for-undefined"
/>
</div>
<div
class="actions-panel"
>
<button
class="Button primary"
data-waiting="false"
disabled=""
type="button"
>
Add clusters
</button>
<div />
</div>
</div>
<div
class="toolsRegion"
>
<div
class="fixed top-[60px]"
>
<div>
<div
aria-label="Close"
class="closeButton"
role="button"
>
<i
class="Icon icon material focusable"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
<div
aria-hidden="true"
class="esc"
>
ESC
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
`;
@@ -0,0 +1,49 @@
/**
* 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 type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
describe("add-cluster - navigation using application menu", () => {
let applicationBuilder: ApplicationBuilder;
let rendered: RenderResult;
beforeEach(async () => {
applicationBuilder = getApplicationBuilder();
rendered = await applicationBuilder.render();
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("does not show add cluster page yet", () => {
const actual = rendered.queryByTestId("add-cluster-page");
expect(actual).toBeNull();
});
describe("when navigating to add cluster using application menu", () => {
beforeEach(async () => {
await applicationBuilder.applicationMenu.click(
"root",
"file",
"add-cluster",
);
});
it("renders", () => {
expect(rendered.container).toMatchSnapshot();
});
it("shows add cluster page", () => {
const actual = rendered.getByTestId("add-cluster-page");
expect(actual).not.toBeNull();
});
});
});
@@ -0,0 +1,132 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`application-menu, given platform is 'darwin' given enough time passes populates application menu 1`] = `
Array [
"root",
"root -> mac",
"root -> mac -> about",
"root -> mac -> separator-1",
"root -> mac -> navigate-to-preferences",
"root -> mac -> navigate-to-extensions",
"root -> mac -> separator-2",
"root -> mac -> services",
"root -> mac -> separator-3",
"root -> mac -> hide",
"root -> mac -> hide-others",
"root -> mac -> unhide",
"root -> mac -> separator-4",
"root -> mac -> quit",
"root -> file",
"root -> file -> add-cluster",
"root -> file -> separator-1-for-file",
"root -> file -> close-window",
"root -> edit",
"root -> edit -> undo",
"root -> edit -> redo",
"root -> edit -> separator-1-in-edit",
"root -> edit -> cut",
"root -> edit -> copy",
"root -> edit -> paste",
"root -> edit -> delete",
"root -> edit -> separator-2-in-edit",
"root -> edit -> selectAll",
"root -> view",
"root -> view -> navigate-to-catalog",
"root -> view -> open-command-palette",
"root -> view -> separator-1-for-view",
"root -> view -> go-back",
"root -> view -> go-forward",
"root -> view -> reload",
"root -> view -> toggle-dev-tools",
"root -> view -> separator-2-for-view",
"root -> view -> reset-zoom",
"root -> view -> zoom-in",
"root -> view -> zoom-out",
"root -> view -> separator-3-for-view",
"root -> view -> toggle-full-screen",
"root -> help",
"root -> help -> navigate-to-welcome",
"root -> help -> open-documentation",
"root -> help -> open-support",
]
`;
exports[`application-menu, given platform is 'linux' given enough time passes populates application menu 1`] = `
Array [
"root",
"root -> file",
"root -> file -> add-cluster",
"root -> file -> navigate-to-preferences",
"root -> file -> navigate-to-extensions",
"root -> file -> quit",
"root -> edit",
"root -> edit -> undo",
"root -> edit -> redo",
"root -> edit -> separator-1-in-edit",
"root -> edit -> cut",
"root -> edit -> copy",
"root -> edit -> paste",
"root -> edit -> delete",
"root -> edit -> separator-2-in-edit",
"root -> edit -> selectAll",
"root -> view",
"root -> view -> navigate-to-catalog",
"root -> view -> open-command-palette",
"root -> view -> separator-1-for-view",
"root -> view -> go-back",
"root -> view -> go-forward",
"root -> view -> reload",
"root -> view -> toggle-dev-tools",
"root -> view -> separator-2-for-view",
"root -> view -> reset-zoom",
"root -> view -> zoom-in",
"root -> view -> zoom-out",
"root -> view -> separator-3-for-view",
"root -> view -> toggle-full-screen",
"root -> help",
"root -> help -> navigate-to-welcome",
"root -> help -> open-documentation",
"root -> help -> open-support",
"root -> help -> about",
]
`;
exports[`application-menu, given platform is 'win32' given enough time passes populates application menu 1`] = `
Array [
"root",
"root -> file",
"root -> file -> add-cluster",
"root -> file -> navigate-to-preferences",
"root -> file -> navigate-to-extensions",
"root -> file -> quit",
"root -> edit",
"root -> edit -> undo",
"root -> edit -> redo",
"root -> edit -> separator-1-in-edit",
"root -> edit -> cut",
"root -> edit -> copy",
"root -> edit -> paste",
"root -> edit -> delete",
"root -> edit -> separator-2-in-edit",
"root -> edit -> selectAll",
"root -> view",
"root -> view -> navigate-to-catalog",
"root -> view -> open-command-palette",
"root -> view -> separator-1-for-view",
"root -> view -> go-back",
"root -> view -> go-forward",
"root -> view -> reload",
"root -> view -> toggle-dev-tools",
"root -> view -> separator-2-for-view",
"root -> view -> reset-zoom",
"root -> view -> zoom-in",
"root -> view -> zoom-out",
"root -> view -> separator-3-for-view",
"root -> view -> toggle-full-screen",
"root -> help",
"root -> help -> navigate-to-welcome",
"root -> help -> open-documentation",
"root -> help -> open-support",
"root -> help -> about",
]
`;
@@ -0,0 +1,226 @@
/**
* 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 { noop } from "lodash/fp";
import { runInAction } from "mobx";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import type { FakeExtensionOptions } from "../../renderer/components/test-utils/get-extension-fake";
import applicationMenuItemInjectionToken from "./main/menu-items/application-menu-item-injection-token";
import logErrorInjectable from "../../common/log-error.injectable";
describe("application-menu-in-legacy-extension-api", () => {
let builder: ApplicationBuilder;
let logErrorMock: jest.Mock;
beforeEach(async () => {
builder = getApplicationBuilder();
builder.beforeApplicationStart(
(mainDi) => {
runInAction(() => {
mainDi.register(
someTopMenuItemInjectable,
someNonExtensionBasedMenuItemInjectable,
);
});
logErrorMock = jest.fn();
mainDi.override(logErrorInjectable, () => logErrorMock);
},
);
await builder.startHidden();
});
describe("when extension with application menu items is enabled", () => {
let onClickMock: jest.Mock;
let testExtensionOptions: FakeExtensionOptions;
beforeEach(() => {
onClickMock = jest.fn();
testExtensionOptions = {
id: "some-test-extension",
name: "some-extension-name",
mainOptions: {
appMenus: [
{
id: "some-non-shown-item",
parentId: "some-top-menu-item",
click: noop,
label: "Irrelevant",
visible: false,
},
{
id: "some-clickable-item",
parentId: "some-top-menu-item",
click: onClickMock,
},
{
parentId: "some-top-menu-item",
type: "separator",
},
{
id: "some-os-action-menu-item-id",
parentId: "some-top-menu-item",
role: "help",
},
{
id: "some-submenu-with-explicit-children",
parentId: "some-top-menu-item",
submenu: [
{ id: "some-explicit-child", label: "Some explicit child", click: noop },
],
},
],
},
};
builder.extensions.enable(testExtensionOptions);
});
it("related menu items exist", () => {
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
(x) =>
x.join(".").startsWith("root.some-top-menu-item.some-extension-name"),
);
expect(menuItemPathsForExtension).toEqual([
["root", "some-top-menu-item", "some-extension-name/some-clickable-item"],
// Note: anonymous index "1" is used by the non-visible menu item.
["root", "some-top-menu-item", "some-extension-name/2-separator"],
["root", "some-top-menu-item", "some-extension-name/some-os-action-menu-item-id"],
["root", "some-top-menu-item", "some-extension-name/some-submenu-with-explicit-children"],
["root", "some-top-menu-item", "some-extension-name/some-submenu-with-explicit-children", "some-extension-name/some-submenu-with-explicit-children/some-explicit-child"],
]);
});
it("when the extension-based clickable menu item is clicked, does so", () => {
builder.applicationMenu.click(
"root", "some-top-menu-item", "some-extension-name/some-clickable-item",
);
expect(onClickMock).toHaveBeenCalled();
});
describe("when the extension is disabled", () => {
beforeEach(() => {
builder.extensions.disable(testExtensionOptions);
});
it("when related menu items no longer exist", () => {
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
(x) =>
x.join(".").startsWith("root.some-top-menu-item.some-extension-name"),
);
expect(menuItemPathsForExtension).toEqual([]);
});
it("when the extension is enabled again, also related menu items exist again", () => {
builder.extensions.enable(testExtensionOptions);
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
(x) =>
x.join(".").startsWith("root.some-top-menu-item.some-extension-name"),
);
expect(menuItemPathsForExtension).toEqual([
["root", "some-top-menu-item", "some-extension-name/some-clickable-item"],
["root", "some-top-menu-item", "some-extension-name/2-separator"],
["root", "some-top-menu-item", "some-extension-name/some-os-action-menu-item-id"],
["root", "some-top-menu-item", "some-extension-name/some-submenu-with-explicit-children"],
["root", "some-top-menu-item", "some-extension-name/some-submenu-with-explicit-children", "some-extension-name/some-submenu-with-explicit-children/some-explicit-child"],
]);
});
});
});
describe("when extension with unrecognizable application menu items is enabled", () => {
beforeEach(() => {
const testExtensionOptions: FakeExtensionOptions = {
id: "some-test-extension",
name: "some-extension-name",
mainOptions: {
appMenus: [
{
id: "some-recognizable-item",
parentId: "some-top-menu-item",
click: noop,
},
{
id: "some-unrecognizable-item",
parentId: "some-top-menu-item",
// Note: there is no way to recognize this
// click: noop,
// role: "help"
// submenu: []
// type: "separator"
},
],
},
};
builder.extensions.enable(testExtensionOptions);
});
it("only recognizable menu items from extension exist", () => {
const menuItemPathsForExtension = builder.applicationMenu.items.filter(
(x) =>
x.join(".").startsWith("root.some-top-menu-item.some-extension-name"),
);
expect(menuItemPathsForExtension).toEqual([
["root", "some-top-menu-item", "some-extension-name/some-recognizable-item"],
]);
});
it("logs about the unrecognizable item", () => {
expect(logErrorMock).toHaveBeenCalledWith(
'[MENU]: Tried to register menu item "some-extension-name/some-unrecognizable-item" but it is not recognizable as any of ApplicationMenuItemTypes',
);
});
});
});
const someTopMenuItemInjectable = getInjectable({
id: "some-top-menu-item",
instantiate: () => ({
id: "some-top-menu-item",
parentId: "root" as const,
kind: "top-level-menu" as const,
label: "Some existing root menu item",
orderNumber: 42,
}),
injectionToken: applicationMenuItemInjectionToken,
});
const someNonExtensionBasedMenuItemInjectable = getInjectable({
id: "some-non-extension-based-menu-item",
instantiate: () => ({
id: "some-non-extension-based-menu-item",
parentId: "some-top-menu-item",
kind: "clickable-menu-item" as const,
label: "Some menu item",
onClick: () => {},
orderNumber: 42,
}),
injectionToken: applicationMenuItemInjectionToken,
});
@@ -0,0 +1,59 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import populateApplicationMenuInjectable from "./main/populate-application-menu.injectable";
import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time";
import { getCompositePaths } from "../../common/utils/composite/get-composite-paths/get-composite-paths";
import platformInjectable, { allPlatforms } from "../../common/vars/platform.injectable";
describe.each(allPlatforms)("application-menu, given platform is '%s'", (platform) => {
let builder: ApplicationBuilder;
let populateApplicationMenuMock: jest.Mock;
beforeEach(async () => {
testUsingFakeTime();
populateApplicationMenuMock = jest.fn();
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
mainDi.override(platformInjectable, () => platform);
mainDi.override(
populateApplicationMenuInjectable,
() => populateApplicationMenuMock,
);
});
await builder.startHidden();
});
it("when insufficient time passes, does not populate menu items yet", () => {
advanceFakeTime(99);
expect(populateApplicationMenuMock).not.toHaveBeenCalled();
});
describe("given enough time passes", () => {
let applicationMenuPaths: string[][];
beforeEach(() => {
advanceFakeTime(100);
applicationMenuPaths = getCompositePaths(
populateApplicationMenuMock.mock.calls[0][0],
);
});
it("populates application menu with at least something", () => {
expect(applicationMenuPaths.length).toBeGreaterThan(0);
});
it("populates application menu", () => {
expect(applicationMenuPaths.map(x => x.join(" -> "))).toMatchSnapshot();
});
});
});
@@ -0,0 +1,81 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import populateApplicationMenuInjectable from "./main/populate-application-menu.injectable";
import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time";
import { getCompositePaths } from "../../common/utils/composite/get-composite-paths/get-composite-paths";
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "./main/menu-items/application-menu-item-injection-token";
import { runInAction } from "mobx";
import logErrorInjectable from "../../common/log-error.injectable";
describe("handling-of-orphan-application-menu-items, given orphan menu item", () => {
let builder: ApplicationBuilder;
let populateApplicationMenuMock: jest.Mock;
let logErrorMock: jest.Mock;
beforeEach(async () => {
testUsingFakeTime();
populateApplicationMenuMock = jest.fn();
logErrorMock = jest.fn();
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
const someOrphanMenuItemInjectable = getInjectable({
id: "some-orphan-menu-item",
instantiate: () => ({
kind: "sub-menu" as const,
id: "some-item-id",
// Note: unknown id makes this item an orphan.
parentId: "some-unknown-parent-id",
orderNumber: 0,
label: "irrelevant",
}),
injectionToken: applicationMenuItemInjectionToken,
});
runInAction(() => {
mainDi.register(someOrphanMenuItemInjectable);
});
mainDi.override(logErrorInjectable, () => logErrorMock);
mainDi.override(
populateApplicationMenuInjectable,
() => populateApplicationMenuMock,
);
});
await builder.startHidden();
});
describe("given some time passes", () => {
let applicationMenuPaths: string[][];
beforeEach(() => {
advanceFakeTime(100);
applicationMenuPaths = getCompositePaths(
populateApplicationMenuMock.mock.calls[0][0],
);
});
it("keeps showing the other application menu items without throwing", () => {
expect(applicationMenuPaths.length).toBeGreaterThan(0);
});
it("does not show orphan application menu item", () => {
expect(applicationMenuPaths.find(x => x.join(".").endsWith("some-item-id")));
});
it("logs about bad menu item", () => {
expect(logErrorMock).toHaveBeenCalledWith('[MENU]: cannot render menu item for missing parentIds: "some-unknown-parent-id"');
});
});
});
@@ -0,0 +1,64 @@
/**
* 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 applicationMenuItemsInjectable from "./application-menu-items.injectable";
import type { Composite } from "../../../common/utils/composite/get-composite/get-composite";
import { getCompositeFor } from "../../../common/utils/composite/get-composite/get-composite";
import { computed } from "mobx";
import { pipeline } from "@ogre-tools/fp";
import type { ApplicationMenuItemTypes } from "./menu-items/application-menu-item-injection-token";
import type { RootComposite } from "../../../common/utils/composite/interfaces";
import type { Discriminable } from "../../../common/utils/composable-responsibilities/discriminable/discriminable";
import { orderByOrderNumber } from "../../../common/utils/composable-responsibilities/orderable/orderable";
import logErrorInjectable from "../../../common/log-error.injectable";
import { isShown } from "../../../common/utils/composable-responsibilities/showable/showable";
export type MenuItemRoot = Discriminable<"root"> & RootComposite<"root">;
const applicationMenuItemCompositeInjectable = getInjectable({
id: "application-menu-item-composite",
instantiate: (di) => {
const menuItems = di.inject(applicationMenuItemsInjectable);
const logError = di.inject(logErrorInjectable);
return computed((): Composite<ApplicationMenuItemTypes | MenuItemRoot> => {
const items = menuItems.get();
return pipeline(
[
{
parentId: undefined,
id: "root",
kind: "root",
} as const,
...items,
],
getCompositeFor({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
transformChildren: (children) =>
pipeline(
children,
orderByOrderNumber,
(children) => children.filter(isShown),
),
handleMissingParentIds: ({ missingParentIds }) => {
logError(
`[MENU]: cannot render menu item for missing parentIds: "${missingParentIds.join(
'", "',
)}"`,
);
},
}),
);
});
},
});
export default applicationMenuItemCompositeInjectable;
@@ -0,0 +1,163 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { computed } from "mobx";
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensExtension } from "../../../extensions/lens-extension";
import type { LensMainExtension } from "../../../extensions/lens-main-extension";
import type {
ApplicationMenuItemTypes,
ClickableMenuItem,
OsActionMenuItem,
Separator,
} from "./menu-items/application-menu-item-injection-token";
import applicationMenuItemInjectionToken from "./menu-items/application-menu-item-injection-token";
import type { MenuRegistration } from "./menu-registration";
import logErrorInjectable from "../../../common/log-error.injectable";
const applicationMenuItemRegistratorInjectable = getInjectable({
id: "application-menu-item-registrator",
instantiate: (di) => {
const logError = di.inject(logErrorInjectable);
const toRecursedInjectables = toRecursedInjectablesFor(logError);
return (ext: LensExtension) => {
const mainExtension = ext as LensMainExtension;
return computed(() => {
const appMenus = Array.isArray(mainExtension.appMenus) ? mainExtension.appMenus : mainExtension.appMenus.get();
return appMenus.flatMap(
toRecursedInjectables([mainExtension.sanitizedExtensionId]),
);
});
};
},
injectionToken: extensionRegistratorInjectionToken,
});
export default applicationMenuItemRegistratorInjectable;
const toRecursedInjectablesFor = (logError: (errorMessage: string) => void) => {
const toRecursedInjectables = (previousIdPath: string[]) =>
(
registration: MenuRegistration,
index: number,
// Todo: new version of injectable would require less type parameters with defaults.
): Injectable<
ApplicationMenuItemTypes,
ApplicationMenuItemTypes,
void
>[] => {
const previousIdPathString = previousIdPath.join("/");
const registrationId = registration.id || index.toString();
const currentIdPath = [...previousIdPath, registrationId];
const currentIdPathString = currentIdPath.join("/");
const parentId = registration.parentId || previousIdPathString;
const menuItem = getApplicationMenuItem({
registration,
parentId,
currentIdPathString,
index,
});
if (!menuItem) {
logError(`[MENU]: Tried to register menu item "${currentIdPathString}" but it is not recognizable as any of ApplicationMenuItemTypes`);
return [];
}
return [
getInjectable({
id: `${currentIdPathString}/application-menu-item`,
instantiate: () => menuItem,
injectionToken: applicationMenuItemInjectionToken,
}),
...((registration.submenu as MenuRegistration[])
? (registration.submenu as MenuRegistration[]).flatMap(
toRecursedInjectables(currentIdPath),
)
: []),
];
};
return toRecursedInjectables;
};
const getApplicationMenuItem = ({
registration,
index,
currentIdPathString,
parentId,
}: {
registration: MenuRegistration;
index: number;
currentIdPathString: string;
parentId: string;
}): ApplicationMenuItemTypes | undefined => {
const orderNumber = 1000 + index * 10;
if (registration.type === "separator") {
return {
kind: "separator" as const,
id: `${currentIdPathString}-separator`,
parentId,
orderNumber,
} as Separator;
}
if (registration.submenu) {
return {
kind: "sub-menu" as const,
id: currentIdPathString,
parentId,
isShown: registration.visible ?? true,
orderNumber,
label: registration.label || "",
};
}
if (registration.click) {
return {
kind: "clickable-menu-item" as const,
id: currentIdPathString,
parentId,
// Todo: hide electron events from this abstraction.
onClick: registration.click,
label: registration.label,
isShown: registration.visible ?? true,
orderNumber,
...(registration.accelerator
? { keyboardShortcut: registration.accelerator as string }
: {}),
} as ClickableMenuItem;
}
if (registration.role) {
return {
kind: "os-action-menu-item" as const,
id: currentIdPathString,
parentId,
label: registration.label,
isShown: registration.visible ?? true,
orderNumber,
actionName: registration.role,
...(registration.accelerator
? { keyboardShortcut: registration.accelerator as string }
: {}),
} as OsActionMenuItem;
}
return undefined;
};
@@ -0,0 +1,28 @@
/**
* 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 { MenuItemConstructorOptions } from "electron";
import { computed } from "mobx";
import applicationMenuItemInjectionToken from "./menu-items/application-menu-item-injection-token";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
export interface MenuItemOpts extends MenuItemConstructorOptions {
submenu?: MenuItemConstructorOptions[];
}
const applicationMenuItemsInjectable = getInjectable({
id: "application-menu-items",
instantiate: (di) => {
const computedInjectMany = di.inject(computedInjectManyInjectable);
return computed(() =>
computedInjectMany(applicationMenuItemInjectionToken).get(),
);
},
});
export default applicationMenuItemsInjectable;
@@ -0,0 +1,27 @@
/**
* 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 { autorun } from "mobx";
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
import populateApplicationMenuInjectable from "./populate-application-menu.injectable";
import applicationMenuItemCompositeInjectable from "./application-menu-item-composite.injectable";
const applicationMenuReactivityInjectable = getInjectable({
id: "application-menu-reactivity",
instantiate: (di) => {
const applicationMenuItemComposite = di.inject(applicationMenuItemCompositeInjectable);
const populateApplicationMenu = di.inject(populateApplicationMenuInjectable);
return getStartableStoppable(
"application-menu-reactivity",
() => autorun(() => populateApplicationMenu(applicationMenuItemComposite.get()), {
delay: 100,
}),
);
},
});
export default applicationMenuReactivityInjectable;
@@ -0,0 +1,91 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { BrowserWindow, KeyboardEvent, MenuItemConstructorOptions, MenuItem as ElectronMenuItem } from "electron";
import type { SetOptional } from "type-fest";
import type { ChildOfParentComposite, ParentOfChildComposite } from "../../../../common/utils/composite/interfaces";
import type { MaybeShowable } from "../../../../common/utils/composable-responsibilities/showable/showable";
import type { Discriminable } from "../../../../common/utils/composable-responsibilities/discriminable/discriminable";
import type { Orderable } from "../../../../common/utils/composable-responsibilities/orderable/orderable";
export interface MayHaveKeyboardShortcut {
keyboardShortcut?: string;
}
export interface ElectronClickable {
// TODO: This leaky abstraction is exposed in Extension API, therefore cannot be updated
onClick: (menuItem: ElectronMenuItem, browserWindow: (BrowserWindow) | (undefined), event: KeyboardEvent) => void;
}
export interface Labeled {
label: string;
}
export interface MaybeLabeled extends SetOptional<Labeled, "label"> {}
type ApplicationMenuItemType<T extends string> =
// Note: "kind" is being used for Discriminated unions of TypeScript to achieve type narrowing.
// See: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions
& Discriminable<T>
& ParentOfChildComposite
& ChildOfParentComposite
& MaybeShowable
& Orderable;
export type TopLevelMenu =
& ApplicationMenuItemType<"top-level-menu">
& { parentId: "root" }
& Labeled
& MayHaveElectronRole;
interface MayHaveElectronRole {
role?: ElectronRoles;
}
type ElectronRoles = Exclude<MenuItemConstructorOptions["role"], undefined>;
export type SubMenu =
& ApplicationMenuItemType<"sub-menu">
& Labeled
& ChildOfParentComposite;
export type ClickableMenuItem =
& ApplicationMenuItemType<"clickable-menu-item">
& MenuItem
& Labeled
& ElectronClickable;
export type OsActionMenuItem =
& ApplicationMenuItemType<"os-action-menu-item">
& MenuItem
& MaybeLabeled
& TriggersElectronAction;
type MenuItem =
& ChildOfParentComposite
& MayHaveKeyboardShortcut;
interface TriggersElectronAction {
actionName: ElectronRoles;
}
// Todo: SeparatorMenuItem
export type Separator =
& ApplicationMenuItemType<"separator">
& ChildOfParentComposite;
export type ApplicationMenuItemTypes =
| TopLevelMenu
| SubMenu
| OsActionMenuItem
| ClickableMenuItem
| Separator
;
const applicationMenuItemInjectionToken = getInjectionToken<ApplicationMenuItemTypes>({
id: "application-menu-item-injection-token",
});
export default applicationMenuItemInjectionToken;
@@ -0,0 +1,22 @@
/**
* 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 applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
const editMenuItemInjectable = getInjectable({
id: "edit-application-menu-item",
instantiate: () => ({
kind: "top-level-menu" as const,
id: "edit",
parentId: "root" as const,
orderNumber: 30,
label: "Edit",
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default editMenuItemInjectable;
@@ -0,0 +1,69 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getApplicationMenuOperationSystemActionInjectable } from "../../get-application-menu-operation-system-action-injectable";
import { getApplicationMenuSeparatorInjectable } from "../../get-application-menu-separator-injectable";
export const actionForUndo = getApplicationMenuOperationSystemActionInjectable({
id: "undo",
parentId: "edit",
orderNumber: 10,
actionName: "undo",
});
export const actionForRedo = getApplicationMenuOperationSystemActionInjectable({
id: "redo",
parentId: "edit",
orderNumber: 20,
actionName: "redo",
});
export const separator1 = getApplicationMenuSeparatorInjectable({
id: "separator-1-in-edit",
parentId: "edit",
orderNumber: 30,
});
export const actionForCut = getApplicationMenuOperationSystemActionInjectable({
id: "cut",
parentId: "edit",
orderNumber: 40,
actionName: "cut",
});
export const actionForCopy = getApplicationMenuOperationSystemActionInjectable({
id: "copy",
parentId: "edit",
orderNumber: 50,
actionName: "copy",
});
export const actionForPaste = getApplicationMenuOperationSystemActionInjectable({
id: "paste",
parentId: "edit",
orderNumber: 60,
actionName: "paste",
});
export const actionForDelete = getApplicationMenuOperationSystemActionInjectable({
id: "delete",
parentId: "edit",
orderNumber: 70,
actionName: "delete",
});
export const separator2 = getApplicationMenuSeparatorInjectable({
id: "separator-2-in-edit",
parentId: "edit",
orderNumber: 80,
});
export const actionForSelectAll = getApplicationMenuOperationSystemActionInjectable({
id: "selectAll",
parentId: "edit",
orderNumber: 90,
actionName: "selectAll",
});
@@ -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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import navigateToAddClusterInjectable from "../../../../../../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable";
const addClusterMenuItemInjectable = getInjectable({
id: "add-cluster-application-menu-item",
instantiate: (di) => {
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "file",
id: "add-cluster",
orderNumber: 10,
label: "Add Cluster",
keyboardShortcut: "CmdOrCtrl+Shift+A",
onClick: () => {
navigateToAddCluster();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default addClusterMenuItemInjectable;
@@ -0,0 +1,30 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import isMacInjectable from "../../../../../../common/vars/is-mac.injectable";
const closeWindowMenuItemInjectable = getInjectable({
id: "close-window-application-menu-item",
instantiate: (di) => {
const isMac = di.inject(isMacInjectable);
return {
id: "close-window",
kind: "os-action-menu-item" as const,
parentId: "file",
orderNumber: 60,
actionName: "close" as const,
label: "Close Window",
keyboardShortcut: "Shift+Cmd+W",
isShown: isMac,
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default closeWindowMenuItemInjectable;
@@ -0,0 +1,22 @@
/**
* 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 applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
const fileMenuItemInjectable = getInjectable({
id: "file-application-menu-item",
instantiate: () => ({
kind: "top-level-menu" as const,
id: "file",
parentId: "root" as const,
orderNumber: 20,
label: "File",
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default fileMenuItemInjectable;
@@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuSeparatorInjectable,
} from "../../get-application-menu-separator-injectable";
export const separator1 = getApplicationMenuSeparatorInjectable({
id: "separator-1-for-file",
parentId: "file",
orderNumber: 20,
isShownOnlyOnMac: true,
});
@@ -0,0 +1,25 @@
/**
* 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 { OsActionMenuItem } from "./application-menu-item-injection-token";
import applicationMenuItemInjectionToken from "./application-menu-item-injection-token";
const getApplicationMenuOperationSystemActionInjectable = ({
id,
...rest
}: Omit<OsActionMenuItem, "kind" >) =>
getInjectable({
id: `application-menu-operation-system-action/${id}`,
instantiate: () => ({
...rest,
id,
kind: "os-action-menu-item" as const,
}),
injectionToken: applicationMenuItemInjectionToken,
});
export { getApplicationMenuOperationSystemActionInjectable };
@@ -0,0 +1,36 @@
/**
* 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 { Separator } from "./application-menu-item-injection-token";
import applicationMenuItemInjectionToken from "./application-menu-item-injection-token";
import isMacInjectable from "../../../../common/vars/is-mac.injectable";
const getApplicationMenuSeparatorInjectable = ({
id,
isShownOnlyOnMac = false,
...rest
}: { isShownOnlyOnMac?: boolean } & Omit<
Separator,
"kind" | "isShown"
>) =>
getInjectable({
id: `application-menu-separator/${id}`,
instantiate: (di) => {
const isMac = di.inject(isMacInjectable);
const isShown = isShownOnlyOnMac ? isMac : true;
return {
...rest,
id,
kind: "separator" as const,
isShown,
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export { getApplicationMenuSeparatorInjectable };
@@ -0,0 +1,23 @@
/**
* 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 applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
const helpMenuItemInjectable = getInjectable({
id: "help-application-menu-item",
instantiate: () => ({
kind: "top-level-menu" as const,
id: "help",
parentId: "root" as const,
orderNumber: 50,
label: "Help",
role: "help" as const,
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default helpMenuItemInjectable;
@@ -0,0 +1,31 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import navigateToWelcomeInjectable from "../../../../../../common/front-end-routing/routes/welcome/navigate-to-welcome.injectable";
const navigateToWelcomeMenuItem = getInjectable({
id: "navigate-to-welcome-menu-item",
instantiate: (di) => {
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "help",
id: "navigate-to-welcome",
orderNumber: 10,
label: "Welcome",
onClick: () => {
navigateToWelcome();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default navigateToWelcomeMenuItem;
@@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import { docsUrl } from "../../../../../../common/vars";
import openLinkInBrowserInjectable from "../../../../../../common/utils/open-link-in-browser.injectable";
import loggerInjectable from "../../../../../../common/logger.injectable";
const openDocumentationMenuItemInjectable = getInjectable({
id: "open-documentation-menu-item",
instantiate: (di) => {
const openLinkInBrowser = di.inject(openLinkInBrowserInjectable);
const logger = di.inject(loggerInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "help",
id: "open-documentation",
orderNumber: 20,
label: "Documentation",
// TODO: Convert to async/await
onClick: () => {
openLinkInBrowser(docsUrl).catch((error) => {
logger.error("[MENU]: failed to open browser", { error });
});
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default openDocumentationMenuItemInjectable;
@@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import { supportUrl } from "../../../../../../common/vars";
import openLinkInBrowserInjectable from "../../../../../../common/utils/open-link-in-browser.injectable";
import loggerInjectable from "../../../../../../common/logger.injectable";
const openSupportItemInjectable = getInjectable({
id: "open-support-menu-item",
instantiate: (di) => {
const openLinkInBrowser = di.inject(openLinkInBrowserInjectable);
const logger = di.inject(loggerInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "help",
id: "open-support",
orderNumber: 30,
label: "Support",
// TODO: Convert to async/await
onClick: () => {
openLinkInBrowser(supportUrl).catch((error) => {
logger.error("[MENU]: failed to open browser", { error });
});
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default openSupportItemInjectable;
@@ -0,0 +1,34 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import navigateToExtensionsInjectable from "../../../../../../common/front-end-routing/routes/extensions/navigate-to-extensions.injectable";
import isMacInjectable from "../../../../../../common/vars/is-mac.injectable";
const navigateToExtensionsMenuItem = getInjectable({
id: "navigate-to-extensions-menu-item",
instantiate: (di) => {
const navigateToExtensions = di.inject(navigateToExtensionsInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: isMac ? "mac" : "file",
id: "navigate-to-extensions",
orderNumber: isMac ? 50 : 40,
label: "Extensions",
keyboardShortcut: isMac ? "CmdOrCtrl+Shift+E" : "Ctrl+Shift+E",
onClick: () => {
navigateToExtensions();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default navigateToExtensionsMenuItem;
@@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuOperationSystemActionInjectable,
} from "../../get-application-menu-operation-system-action-injectable";
export const actionForServices = getApplicationMenuOperationSystemActionInjectable({
id: "services",
parentId: "mac",
orderNumber: 80,
actionName: "services",
});
export const actionForHide = getApplicationMenuOperationSystemActionInjectable({
id: "hide",
parentId: "mac",
orderNumber: 100,
actionName: "hide",
});
export const actionForHideOthers = getApplicationMenuOperationSystemActionInjectable({
id: "hide-others",
parentId: "mac",
orderNumber: 110,
actionName: "hideOthers",
});
export const actionForUnhide = getApplicationMenuOperationSystemActionInjectable({
id: "unhide",
parentId: "mac",
orderNumber: 120,
actionName: "unhide",
});
@@ -0,0 +1,30 @@
/**
* 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 applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
import appNameInjectable from "../../../../../common/vars/app-name.injectable";
import isMacInjectable from "../../../../../common/vars/is-mac.injectable";
const primaryMenuItemInjectable = getInjectable({
id: "primary-application-menu-item",
instantiate: (di) => {
const appName = di.inject(appNameInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "top-level-menu" as const,
parentId: "root" as const,
id: "mac",
orderNumber: 10,
label: appName,
isShown: isMac,
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default primaryMenuItemInjectable;
@@ -0,0 +1,35 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import stopServicesAndExitAppInjectable from "../../../../../../main/stop-services-and-exit-app.injectable";
import isMacInjectable from "../../../../../../common/vars/is-mac.injectable";
const quitApplicationMenuItemInjectable = getInjectable({
id: "quit-application-menu-item",
instantiate: (di) => {
const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "clickable-menu-item" as const,
id: "quit",
label: "Quit",
parentId: isMac ? "mac" : "file",
orderNumber: isMac ? 140 : 70,
keyboardShortcut: isMac ? "Cmd+Q" : "Alt+F4",
onClick: () => {
stopServicesAndExitApp();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default quitApplicationMenuItemInjectable;
@@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuSeparatorInjectable,
} from "../../get-application-menu-separator-injectable";
export const separator1 = getApplicationMenuSeparatorInjectable({
id: "separator-1",
parentId: "mac",
orderNumber: 30,
});
export const separator2 = getApplicationMenuSeparatorInjectable({
id: "separator-2",
parentId: "mac",
orderNumber: 70,
});
export const separator3 = getApplicationMenuSeparatorInjectable({
id: "separator-3",
parentId: "mac",
orderNumber: 90,
});
export const separator4 = getApplicationMenuSeparatorInjectable({
id: "separator-4",
parentId: "mac",
orderNumber: 130,
});
@@ -0,0 +1,35 @@
/**
* 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 productNameInjectable from "../../../../../../common/vars/product-name.injectable";
import showAboutInjectable from "./show-about.injectable";
import applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import isMacInjectable from "../../../../../../common/vars/is-mac.injectable";
const aboutMenuItemInjectable = getInjectable({
id: "about-menu-item",
instantiate: (di) => {
const productName = di.inject(productNameInjectable);
const showAbout = di.inject(showAboutInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "clickable-menu-item" as const,
id: "about",
parentId: isMac ? "mac" : "help",
orderNumber: isMac ? 10 : 40,
label: `About ${productName}`,
onClick() {
showAbout();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default aboutMenuItemInjectable;
@@ -0,0 +1,45 @@
/**
* 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 showMessagePopupInjectable from "../../../../../../main/electron-app/features/show-message-popup.injectable";
import isWindowsInjectable from "../../../../../../common/vars/is-windows.injectable";
import appNameInjectable from "../../../../../../common/vars/app-name.injectable";
import productNameInjectable from "../../../../../../common/vars/product-name.injectable";
import buildVersionInjectable from "../../../../../../main/vars/build-version/build-version.injectable";
import extensionApiVersionInjectable from "../../../../../../common/vars/extension-api-version.injectable";
import applicationCopyrightInjectable from "../../../../../../common/vars/application-copyright.injectable";
const showAboutInjectable = getInjectable({
id: "show-about",
instantiate: (di) => {
const buildVersion = di.inject(buildVersionInjectable);
const extensionApiVersion = di.inject(extensionApiVersionInjectable);
const showMessagePopup = di.inject(showMessagePopupInjectable);
const isWindows = di.inject(isWindowsInjectable);
const appName = di.inject(appNameInjectable);
const productName = di.inject(productNameInjectable);
const applicationCopyright = di.inject(applicationCopyrightInjectable);
return () => {
const appInfo = [
`${appName}: ${buildVersion.get()}`,
`Extension API: ${extensionApiVersion}`,
`Electron: ${process.versions.electron}`,
`Chrome: ${process.versions.chrome}`,
`Node: ${process.versions.node}`,
applicationCopyright,
];
showMessagePopup(
`${isWindows ? " ".repeat(2) : ""}${appName}`,
productName,
appInfo.join("\r\n"),
);
};
},
});
export default showAboutInjectable;
@@ -0,0 +1,31 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import { webContents } from "electron";
const goBackMenuItemInjectable = getInjectable({
id: "go-back-menu-item",
instantiate: () => ({
kind: "clickable-menu-item" as const,
parentId: "view",
id: "go-back",
orderNumber: 40,
label: "Back",
keyboardShortcut: "CmdOrCtrl+[",
onClick: () => {
webContents
.getAllWebContents()
.filter((wc) => wc.getType() === "window")
.forEach((wc) => wc.goBack());
},
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default goBackMenuItemInjectable;
@@ -0,0 +1,31 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import { webContents } from "electron";
const goForwardMenuItemInjectable = getInjectable({
id: "go-forward-menu-item",
instantiate: () => ({
kind: "clickable-menu-item" as const,
parentId: "view",
id: "go-forward",
orderNumber: 50,
label: "Forward",
keyboardShortcut: "CmdOrCtrl+]",
onClick: () => {
webContents
.getAllWebContents()
.filter((wc) => wc.getType() === "window")
.forEach((wc) => wc.goForward());
},
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default goForwardMenuItemInjectable;
@@ -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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import navigateToCatalogInjectable from "../../../../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
const navigateToCatalogMenuItemInjectable = getInjectable({
id: "navigate-to-catalog-menu-item",
instantiate: (di) => {
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "view",
id: "navigate-to-catalog",
orderNumber: 10,
label: "Catalog",
keyboardShortcut: "Shift+CmdOrCtrl+C",
onClick: () => {
navigateToCatalog();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default navigateToCatalogMenuItemInjectable;
@@ -0,0 +1,40 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import broadcastMessageInjectable from "../../../../../../common/ipc/broadcast-message.injectable";
const openCommandPaletteMenuItemInjectable = getInjectable({
id: "open-command-palette-menu-item",
instantiate: (di) => {
const broadcastMessage = di.inject(broadcastMessageInjectable);
return {
kind: "clickable-menu-item" as const,
parentId: "view",
id: "open-command-palette",
orderNumber: 20,
label: "Command Palette...",
keyboardShortcut: "Shift+CmdOrCtrl+P",
onClick(_m, _b, event) {
/**
* Don't broadcast unless it was triggered by menu iteration so that
* there aren't double events in renderer
*
* NOTE: this `?` is required because of a bug in playwright. https://github.com/microsoft/playwright/issues/10554
*/
if (!event?.triggeredByAccelerator) {
broadcastMessage("command-palette:open");
}
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default openCommandPaletteMenuItemInjectable;
@@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuOperationSystemActionInjectable,
} from "../../get-application-menu-operation-system-action-injectable";
export const actionForToggleDevTools = getApplicationMenuOperationSystemActionInjectable({
id: "toggle-dev-tools",
parentId: "view",
orderNumber: 70,
actionName: "toggleDevTools",
});
export const actionForResetZoom = getApplicationMenuOperationSystemActionInjectable({
id: "reset-zoom",
parentId: "view",
orderNumber: 90,
actionName: "resetZoom",
});
export const actionForZoomIn = getApplicationMenuOperationSystemActionInjectable({
id: "zoom-in",
parentId: "view",
orderNumber: 100,
actionName: "zoomIn",
});
export const actionForZoomOut = getApplicationMenuOperationSystemActionInjectable({
id: "zoom-out",
parentId: "view",
orderNumber: 110,
actionName: "zoomOut",
});
export const actionForToggleFullScreen = getApplicationMenuOperationSystemActionInjectable({
id: "toggle-full-screen",
parentId: "view",
orderNumber: 130,
actionName: "togglefullscreen",
});
@@ -0,0 +1,34 @@
/**
* 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 applicationMenuItemInjectionToken from "../../application-menu-item-injection-token";
import reloadCurrentApplicationWindowInjectable from "../../../../../../main/start-main-application/lens-window/reload-current-application-window.injectable";
const reloadMenuItemInjectable = getInjectable({
id: "reload-menu-item",
instantiate: (di) => {
const reloadApplicationWindow = di.inject(
reloadCurrentApplicationWindowInjectable,
);
return {
kind: "clickable-menu-item" as const,
parentId: "view",
id: "reload",
orderNumber: 60,
label: "Reload",
keyboardShortcut: "CmdOrCtrl+R",
onClick: () => {
reloadApplicationWindow();
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default reloadMenuItemInjectable;
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getApplicationMenuSeparatorInjectable,
} from "../../get-application-menu-separator-injectable";
export const separator1 = getApplicationMenuSeparatorInjectable({
id: "separator-1-for-view",
parentId: "view",
orderNumber: 30,
});
export const separator2 = getApplicationMenuSeparatorInjectable({
id: "separator-2-for-view",
parentId: "view",
orderNumber: 80,
});
export const separator3 = getApplicationMenuSeparatorInjectable({
id: "separator-3-for-view",
parentId: "view",
orderNumber: 120,
});
@@ -0,0 +1,22 @@
/**
* 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 applicationMenuItemInjectionToken from "../application-menu-item-injection-token";
const viewMenuItemInjectable = getInjectable({
id: "view-application-menu-item",
instantiate: () => ({
kind: "top-level-menu" as const,
parentId: "root" as const,
id: "view",
orderNumber: 40,
label: "View",
}),
injectionToken: applicationMenuItemInjectionToken,
});
export default viewMenuItemInjectable;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { MenuItemConstructorOptions } from "electron";
import type { IComputedValue } from "mobx";
export type MenuRegistration = {
parentId: string;
visible?: IComputedValue<boolean> | boolean;
} & Omit<MenuItemConstructorOptions, "visible">;
@@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Menu } from "electron";
import type { MenuItemOpts } from "./application-menu-items.injectable";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
export function buildMenu(applicationMenuItems: MenuItemOpts[]) {
Menu.setApplicationMenu(
Menu.buildFromTemplate(applicationMenuItems),
);
}
@@ -0,0 +1,8 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import populateApplicationMenuInjectable from "./populate-application-menu.injectable";
import { getGlobalOverride } from "../../../common/test-utils/get-global-override";
export default getGlobalOverride(populateApplicationMenuInjectable, () => () => {});
@@ -0,0 +1,110 @@
/**
* 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 { Menu } from "electron";
import type { MenuItemOpts } from "./application-menu-items.injectable";
import type { Composite } from "../../../common/utils/composite/get-composite/get-composite";
import type { ApplicationMenuItemTypes } from "./menu-items/application-menu-item-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { map, sortBy } from "lodash/fp";
import type { MenuItemRoot } from "./application-menu-item-composite.injectable";
import { checkThatAllDiscriminablesAreExhausted } from "../../../common/utils/composable-responsibilities/discriminable/discriminable";
const populateApplicationMenuInjectable = getInjectable({
id: "populate-application-menu",
instantiate:
() => (composite: Composite<ApplicationMenuItemTypes | MenuItemRoot>) => {
const electronTemplate = getApplicationMenuTemplate(composite);
const menu = Menu.buildFromTemplate(electronTemplate);
Menu.setApplicationMenu(menu);
},
causesSideEffects: true,
});
export default populateApplicationMenuInjectable;
export const getApplicationMenuTemplate = (composite: Composite<ApplicationMenuItemTypes | MenuItemRoot>) => {
const topLevelMenus = composite.children.filter(
(x): x is Composite<ApplicationMenuItemTypes> => x.value.kind !== "root",
);
return topLevelMenus.map(toHierarchicalElectronMenuItem);
};
const toHierarchicalElectronMenuItem = (
composite: Composite<ApplicationMenuItemTypes>,
): MenuItemOpts => {
const value = composite.value;
switch (value.kind) {
case "top-level-menu": {
const { id } = composite;
const { label, role } = value;
return {
...(id ? { id } : {}),
...(role ? { role } : {}),
label,
submenu: pipeline(
composite.children,
sortBy((childComposite) => childComposite.value.orderNumber),
map(toHierarchicalElectronMenuItem),
),
};
}
case "sub-menu": {
const { id } = composite;
const { label } = value;
return {
...(id ? { id } : {}),
label,
submenu: pipeline(
composite.children,
sortBy((childComposite) => childComposite.value.orderNumber),
map(toHierarchicalElectronMenuItem),
),
};
}
case "clickable-menu-item": {
const { id } = composite;
const { label, onClick, keyboardShortcut } = value;
return {
...(id ? { id } : {}),
...(label ? { label } : {}),
...(keyboardShortcut ? { accelerator: keyboardShortcut }: {}),
click: onClick,
};
}
case "os-action-menu-item": {
const { label, keyboardShortcut, actionName } = value;
return {
...(label ? { label } : {}),
...(keyboardShortcut ? { accelerator: keyboardShortcut } : {}),
role: actionName,
};
}
case "separator": {
return {
type: "separator",
};
}
default: {
throw checkThatAllDiscriminablesAreExhausted(value);
}
}
};
@@ -0,0 +1,28 @@
/**
* 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 applicationMenuReactivityInjectable from "./application-menu-reactivity.injectable";
import { onLoadOfApplicationInjectionToken } from "../../../main/start-main-application/runnable-tokens/on-load-of-application-injection-token";
const startApplicationMenuInjectable = getInjectable({
id: "start-application-menu",
instantiate: (di) => {
const applicationMenu = di.inject(
applicationMenuReactivityInjectable,
);
return {
id: "start-application-menu",
run: () => {
applicationMenu.start();
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default startApplicationMenuInjectable;
@@ -0,0 +1,24 @@
/**
* 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 applicationMenuReactivityInjectable from "./application-menu-reactivity.injectable";
import { beforeQuitOfBackEndInjectionToken } from "../../../main/start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
const stopApplicationMenuInjectable = getInjectable({
id: "stop-application-menu",
instantiate: (di) => {
const applicationMenu = di.inject(applicationMenuReactivityInjectable);
return {
id: "stop-application-menu",
run: () => void applicationMenu.stop(),
};
},
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopApplicationMenuInjectable;
@@ -0,0 +1,266 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "./main/updating-is-enabled/publish-is-configured/publish-is-configured.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { CheckForPlatformUpdates } from "./main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "./main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import appEventBusInjectable from "../../common/app-event-bus/app-event-bus.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import processCheckingForUpdatesInjectable from "./main/process-checking-for-updates.injectable";
import type { DownloadPlatformUpdate } from "./main/download-update/download-platform-update/download-platform-update.injectable";
import downloadPlatformUpdateInjectable from "./main/download-update/download-platform-update/download-platform-update.injectable";
import quitAndInstallUpdateInjectable from "./main/quit-and-install-update.injectable";
import periodicalCheckForUpdatesInjectable from "./child-features/periodical-checking-of-updates/main/periodical-check-for-updates.injectable";
import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time";
import emitEventInjectable from "../../common/app-event-bus/emit-event.injectable";
import getBuildVersionInjectable from "../../main/vars/build-version/get-build-version.injectable";
describe("analytics for installing update", () => {
let builder: ApplicationBuilder;
let checkForPlatformUpdatesMock: AsyncFnMock<CheckForPlatformUpdates>;
let downloadPlatformUpdateMock: AsyncFnMock<DownloadPlatformUpdate>;
let analyticsListenerMock: jest.Mock;
let mainDi: DiContainer;
beforeEach(async () => {
testUsingFakeTime("2015-10-21T07:28:00Z");
builder = getApplicationBuilder();
analyticsListenerMock = jest.fn();
builder.beforeApplicationStart(mainDi => {
mainDi.override(getBuildVersionInjectable, () => () => "42.0.0");
checkForPlatformUpdatesMock = asyncFn();
mainDi.override(
checkForPlatformUpdatesInjectable,
() => checkForPlatformUpdatesMock,
);
downloadPlatformUpdateMock = asyncFn();
mainDi.override(downloadPlatformUpdateInjectable, () => downloadPlatformUpdateMock);
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
mainDi.unoverride(emitEventInjectable);
const eventBus = mainDi.inject(appEventBusInjectable);
eventBus.addListener(analyticsListenerMock);
});
mainDi = builder.mainDi;
});
describe("given application is started and checking updates periodically", () => {
beforeEach(async () => {
mainDi.unoverride(periodicalCheckForUpdatesInjectable);
mainDi.permitSideEffects(periodicalCheckForUpdatesInjectable);
await builder.render();
});
it("sends event to analytics for being checked periodically", () => {
expect(analyticsListenerMock).toHaveBeenCalledWith({
name: "app",
action: "checking-for-updates",
params: {
currentDateTime: "2015-10-21T07:28:00Z",
source: "periodic",
},
});
});
it("when enough time passes to check for updates again, sends event to analytics for being checked periodically", () => {
analyticsListenerMock.mockClear();
advanceFakeTime(1000 * 60 * 60 * 2);
expect(analyticsListenerMock).toHaveBeenCalledWith({
name: "app",
action: "checking-for-updates",
params: {
currentDateTime: "2015-10-21T09:28:00Z",
source: "periodic",
},
});
});
});
describe("when application is started", () => {
beforeEach(async () => {
analyticsListenerMock.mockClear();
await builder.render();
});
it("sends event to analytics about the current version", () => {
expect(analyticsListenerMock).toHaveBeenCalledWith({
name: "app",
action: "current-version",
params: {
version: "42.0.0",
currentDateTime: "2015-10-21T07:28:00Z",
},
});
});
it("when checking for updates using tray, sends event to analytics for being checked from tray", async () => {
analyticsListenerMock.mockClear();
builder.tray.click("check-for-updates");
expect(analyticsListenerMock.mock.calls).toEqual([
[
{
name: "app",
action: "checking-for-updates",
params: {
currentDateTime: "2015-10-21T07:28:00Z",
source: "tray",
},
},
],
]);
});
it("when checking for updates using application menu, sends event to analytics for being checked from application menu", async () => {
analyticsListenerMock.mockClear();
builder.applicationMenu.click("root", "mac", "check-for-updates");
expect(analyticsListenerMock.mock.calls).toEqual([
[
{
name: "app",
action: "checking-for-updates",
params: {
currentDateTime: "2015-10-21T07:28:00Z",
source: "application-menu",
},
},
],
]);
});
describe("given checking for updates, when check for updates resolves with new update being available", () => {
beforeEach(async () => {
const processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates("irrelevant");
analyticsListenerMock.mockClear();
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "43.0.0",
});
});
it("sends event to analytics about new update being available", () => {
expect(analyticsListenerMock.mock.calls).toEqual([
[
{
name: "app",
action: "update-was-discovered",
params: {
version: "43.0.0",
currentDateTime: "2015-10-21T07:28:00Z",
},
},
],
]);
});
describe("given update is downloaded", () => {
beforeEach(async () => {
analyticsListenerMock.mockClear();
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true });
});
describe("given checking for updates again", () => {
beforeEach(() => {
const processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates("irrelevant");
analyticsListenerMock.mockClear();
});
it("when check resolves with same version that was previously downloaded, does not send event to analytics about update discovered", async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "43.0.0",
});
expect(analyticsListenerMock).not.toHaveBeenCalled();
});
it("when check resolves with different version that was previously downloaded, sends event to analytics about update discovered", async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "44.0.0",
});
expect(analyticsListenerMock.mock.calls).toEqual([
[
{
name: "app",
action: "update-was-discovered",
params: {
version: "44.0.0",
currentDateTime: "2015-10-21T07:28:00Z",
},
},
],
]);
});
});
it("does not send event to analytics about update downloaded being successful", () => {
expect(analyticsListenerMock).not.toHaveBeenCalled();
});
it("when installing the update, sends event to analytics about installing the update", () => {
const quitAndInstallUpdate = mainDi.inject(quitAndInstallUpdateInjectable);
quitAndInstallUpdate();
expect(analyticsListenerMock.mock.calls).toEqual([
[
{
name: "app",
action: "start-installing-update",
params: {
version: "43.0.0",
currentDateTime: "2015-10-21T07:28:00Z",
updateChannel: "latest",
},
},
],
]);
});
});
});
});
});
@@ -0,0 +1,46 @@
/**
* 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 applicationMenuItemInjectionToken from "../../../../application-menu/main/menu-items/application-menu-item-injection-token";
import processCheckingForUpdatesInjectable from "../../../main/process-checking-for-updates.injectable";
import showApplicationWindowInjectable from "../../../../../main/start-main-application/lens-window/show-application-window.injectable";
import updatingIsEnabledInjectable from "../../../main/updating-is-enabled/updating-is-enabled.injectable";
import isMacInjectable from "../../../../../common/vars/is-mac.injectable";
const checkForUpdatesMenuItemInjectable = getInjectable({
id: "check-for-updates-menu-item",
instantiate: (di) => {
const processCheckingForUpdates = di.inject(
processCheckingForUpdatesInjectable,
);
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const updatingIsEnabled = di.inject(updatingIsEnabledInjectable);
const isMac = di.inject(isMacInjectable);
return {
kind: "clickable-menu-item" as const,
id: "check-for-updates",
parentId: isMac ? "mac" : "help",
orderNumber: isMac ? 20 : 50,
label: "Check for updates",
isShown: updatingIsEnabled,
onClick: () => {
// Todo: implement using async/await
processCheckingForUpdates("application-menu").then(() =>
showApplicationWindow(),
);
},
};
},
injectionToken: applicationMenuItemInjectionToken,
});
export default checkForUpdatesMenuItemInjectable;
@@ -0,0 +1,596 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`encourage user to update when sufficient time passed since update was downloaded when started given the update check when update downloaded given some time passes, when checking for updates again when checking for updates resolves with same version that was previously downloaded renders 1`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="home-button"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<button
class="updateButton"
data-testid="update-button"
data-warning-level="light"
id="update-lens-button"
>
Update
<i
class="Icon icon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</button>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to some-product-name!
</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`encourage user to update when sufficient time passed since update was downloaded when started renders 1`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="home-button"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to some-product-name!
</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
@@ -0,0 +1,185 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { RenderResult } from "@testing-library/react";
import { act } from "@testing-library/react";
import type { CheckForPlatformUpdates } from "../../main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "../../main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import type { DownloadPlatformUpdate } from "../../main/download-update/download-platform-update/download-platform-update.injectable";
import downloadPlatformUpdateInjectable from "../../main/download-update/download-platform-update/download-platform-update.injectable";
import publishIsConfiguredInjectable from "../../main/updating-is-enabled/publish-is-configured/publish-is-configured.injectable";
import electronUpdaterIsActiveInjectable from "../../../../main/electron-app/features/electron-updater-is-active.injectable";
import type { ApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import processCheckingForUpdatesInjectable from "../../main/process-checking-for-updates.injectable";
import quitAndInstallUpdateInjectable from "../../main/quit-and-install-update.injectable";
import { advanceFakeTime, testUsingFakeTime } from "../../../../common/test-utils/use-fake-time";
function daysToMilliseconds(days: number) {
return Math.round(days * 24 * 60 * 60 * 1000);
}
describe("encourage user to update when sufficient time passed since update was downloaded", () => {
let applicationBuilder: ApplicationBuilder;
let checkForPlatformUpdatesMock: AsyncFnMock<CheckForPlatformUpdates>;
let downloadPlatformUpdateMock: AsyncFnMock<DownloadPlatformUpdate>;
let quitAndInstallUpdateMock: jest.MockedFunction<() => void>;
beforeEach(() => {
testUsingFakeTime("2015-10-21T07:28:00Z");
applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart((mainDi) => {
checkForPlatformUpdatesMock = asyncFn();
downloadPlatformUpdateMock = asyncFn();
mainDi.override(
checkForPlatformUpdatesInjectable,
() => checkForPlatformUpdatesMock,
);
mainDi.override(
downloadPlatformUpdateInjectable,
() => downloadPlatformUpdateMock,
);
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
quitAndInstallUpdateMock = jest.fn();
mainDi.override(quitAndInstallUpdateInjectable, () => quitAndInstallUpdateMock);
});
});
describe("when started", () => {
let rendered: RenderResult;
beforeEach(async () => {
rendered = await applicationBuilder.render();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show update button yet", () => {
const button = rendered.queryByTestId("update-button");
expect(button).toBeNull();
});
describe("given the update check", () => {
let processCheckingForUpdates: (source: string) => Promise<{ updateIsReadyToBeInstalled: boolean }>;
beforeEach(async () => {
processCheckingForUpdates = applicationBuilder.mainDi.inject(
processCheckingForUpdatesInjectable,
);
processCheckingForUpdates("irrelevant");
});
describe("when update downloaded", () => {
let button: HTMLElement;
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true });
button = rendered.getByTestId("update-button");
});
it("shows update button to help user to update", () => {
expect(button).toBeInTheDocument();
});
it("has soft emotional indication in the button", () => {
expect(button).toHaveAttribute("data-warning-level", "light");
});
it("given closing the application window, when starting the application window again, still shows the button", async () => {
applicationBuilder.applicationWindow.closeAll();
const window = applicationBuilder.applicationWindow.create("some-window-id");
await window.start();
const button = window.rendered.queryByTestId("update-button");
expect(button).toBeInTheDocument();
});
describe("given some time passes, when checking for updates again", () => {
beforeEach(() => {
advanceFakeTime(daysToMilliseconds(2));
processCheckingForUpdates("irrelevant");
});
describe("when checking for updates resolves with same version that was previously downloaded", () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
});
it("when enough time from download passes for medium update encouragement, has medium emotional indication in the button", () => {
advanceFakeTime(daysToMilliseconds(20));
expect(button).toHaveAttribute("data-warning-level", "medium");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
});
describe("when button is clicked", () => {
beforeEach(() => {
act(() => button.click());
});
it("shows dropdown with update item", () => {
expect(rendered.getByTestId("update-lens-menu-item")).toBeInTheDocument();
});
it("when selected update now, restarts the application to update", () => {
const updateMenuItem = rendered.getByTestId("update-lens-menu-item");
act(() => updateMenuItem.click());
expect(quitAndInstallUpdateMock).toBeCalled();
});
it("when dropdown closed without clicking update item, does not restart the application to update", () => {
act(() => button.click());
expect(quitAndInstallUpdateMock).not.toBeCalled();
});
});
it("given just enough time passes for medium update encouragement, has medium emotional indication in the button", () => {
advanceFakeTime(daysToMilliseconds(22));
expect(button).toHaveAttribute("data-warning-level", "medium");
});
it("given just enough time passes for severe update encouragement, has severe emotional indication in the button", () => {
advanceFakeTime(daysToMilliseconds(26));
expect(button).toHaveAttribute("data-warning-level", "high");
});
});
});
});
});
@@ -0,0 +1,28 @@
/**
* 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 { topBarItemOnLeftSideInjectionToken } from "../../../../../../renderer/components/layout/top-bar/top-bar-items/top-bar-item-injection-token";
import { UpdateButton } from "./update-button";
import updateWarningLevelInjectable from "./update-warning-level.injectable";
const updateApplicationTopBarItemInjectable = getInjectable({
id: "update-application-top-bar-item",
instantiate: (di) => {
const warningLevel = di.inject(updateWarningLevelInjectable);
return {
id: "update-application",
isShown: computed(() => !!warningLevel.get()),
orderNumber: 50,
Component: UpdateButton,
};
},
injectionToken: topBarItemOnLeftSideInjectionToken,
});
export default updateApplicationTopBarItemInjectable;
@@ -0,0 +1,6 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./update-button";
@@ -0,0 +1,52 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.updateButton {
--accent-color: var(--colorOk);
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;
cursor: default;
position: relative;
&:hover::before{
opacity: 0.25;
}
&:focus-visible {
box-shadow: 0 0 0 2px var(--blue);
border-color: transparent;
}
&::before {
content: " ";
position: absolute;
background: var(--accent-color);
width: 100%;
height: 100%;
left: 0;
opacity: 0.15;
z-index: -1;
transition: opacity 0.1s;
}
}
.icon {
width: 16px;
height: 16px;
}
.warningMedium {
--accent-color: var(--colorWarning);
}
.warningHigh {
--accent-color: var(--colorSoftError);
}
@@ -0,0 +1,79 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./styles.module.scss";
import type { HTMLAttributes } from "react";
import React, { useState } from "react";
import { Menu, MenuItem } from "../../../../../../../renderer/components/menu";
import { cssNames } from "../../../../../../../renderer/utils";
import type { IconProps } from "../../../../../../../renderer/components/icon";
import { Icon } from "../../../../../../../renderer/components/icon";
import { withInjectables } from "@ogre-tools/injectable-react";
import { observer } from "mobx-react";
import type { IComputedValue } from "mobx";
import restartAndInstallUpdateInjectable from "../../../../../renderer/restart-and-install-update.injectable";
import updateWarningLevelInjectable from "../update-warning-level.injectable";
interface UpdateButtonProps extends HTMLAttributes<HTMLButtonElement> {}
interface Dependencies {
warningLevel: IComputedValue<"light" | "medium" | "high" | "">;
update: () => void;
}
const NonInjectedUpdateButton = observer(({ warningLevel, update, id }: UpdateButtonProps & Dependencies) => {
const buttonId = id ?? "update-lens-button";
const menuIconProps: IconProps = { material: "update", small: true };
const [opened, setOpened] = useState(false);
const level = warningLevel.get();
const toggle = () => {
setOpened(!opened);
};
return (
<>
<button
data-testid="update-button"
data-warning-level={level}
id={buttonId}
className={cssNames(styles.updateButton, {
[styles.warningHigh]: level === "high",
[styles.warningMedium]: level === "medium",
})}
>
Update
<Icon material="arrow_drop_down" className={styles.icon}/>
</button>
<Menu
usePortal
htmlFor={buttonId}
isOpen={opened}
close={toggle}
open={toggle}
>
<MenuItem
icon={menuIconProps}
onClick={update}
data-testid="update-lens-menu-item"
>
Relaunch to Update Lens
</MenuItem>
</Menu>
</>
);
});
export const UpdateButton = withInjectables<Dependencies, UpdateButtonProps>(
NonInjectedUpdateButton,
{
getProps: (di, props) => ({
...props,
warningLevel: di.inject(updateWarningLevelInjectable),
update: di.inject(restartAndInstallUpdateInjectable),
}),
},
);
@@ -0,0 +1,45 @@
/**
* 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 updateDownloadedDateTimeInjectable from "../../../../common/update-downloaded-date-time/update-downloaded-date-time.injectable";
import { reactiveNow } from "../../../../../../common/utils/reactive-now/reactive-now";
const updateWarningLevelInjectable = getInjectable({
id: "update-warning-level",
instantiate: (di) => {
const updateDownloadedDateTime = di.inject(updateDownloadedDateTimeInjectable);
return computed(() => {
const downloadedAt = updateDownloadedDateTime.value.get();
if (!downloadedAt) {
return "";
}
const ONE_DAY = 1000 * 60 * 60 * 24;
const downloadedAtTimestamp = new Date(downloadedAt).getTime();
const currentDateTimeTimestamp = reactiveNow(ONE_DAY);
const elapsedTime = currentDateTimeTimestamp - downloadedAtTimestamp;
const elapsedDays = elapsedTime / ONE_DAY;
if (elapsedDays < 20) {
return "light";
}
if (elapsedDays >= 20 && elapsedDays < 25) {
return "medium";
}
return "high";
});
},
});
export default updateWarningLevelInjectable;
@@ -0,0 +1,282 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import type { RenderResult } from "@testing-library/react";
import electronUpdaterIsActiveInjectable from "../../../../main/electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "../../main/updating-is-enabled/publish-is-configured/publish-is-configured.injectable";
import type { CheckForPlatformUpdates } from "../../main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "../../main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { DownloadPlatformUpdate } from "../../main/download-update/download-platform-update/download-platform-update.injectable";
import downloadPlatformUpdateInjectable from "../../main/download-update/download-platform-update/download-platform-update.injectable";
import type { LensWindow } from "../../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
import getCurrentApplicationWindowInjectable from "../../../../main/start-main-application/lens-window/application-window/get-current-application-window.injectable";
describe("installing update using tray", () => {
let builder: ApplicationBuilder;
let checkForPlatformUpdatesMock: AsyncFnMock<CheckForPlatformUpdates>;
let downloadPlatformUpdateMock: AsyncFnMock<DownloadPlatformUpdate>;
beforeEach(() => {
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
checkForPlatformUpdatesMock = asyncFn();
downloadPlatformUpdateMock = asyncFn();
mainDi.override(
checkForPlatformUpdatesInjectable,
() => checkForPlatformUpdatesMock,
);
mainDi.override(
downloadPlatformUpdateInjectable,
() => downloadPlatformUpdateMock,
);
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
});
});
describe("when started", () => {
let rendered: RenderResult;
beforeEach(async () => {
rendered = await builder.render();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("user cannot install update yet", () => {
expect(builder.tray.get("install-update")).toBeNull();
});
describe("given all application windows are closed, when checking for updates", () => {
let getCurrentApplicationWindow: () => LensWindow | undefined;
beforeEach(() => {
getCurrentApplicationWindow = builder.mainDi.inject(
getCurrentApplicationWindowInjectable,
);
builder.applicationWindow.closeAll();
builder.tray.click("check-for-updates");
});
describe("when check for update resolves with new update", () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
});
it("does not show application window yet", () => {
const actual = getCurrentApplicationWindow();
expect(actual).toBeUndefined();
});
describe("when download of update resolves with success", () => {
beforeEach(async () => {
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true });
});
it("shows the application window", () => {
const actual = getCurrentApplicationWindow();
expect(actual).not.toBeUndefined();
});
it("given closing application window again and checking for updates again using tray, when check resolves with same version that was earlier downloaded, shows the application window", async () => {
builder.applicationWindow.closeAll();
builder.tray.click("check-for-updates");
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
const actual = getCurrentApplicationWindow();
expect(actual).not.toBeUndefined();
});
});
it("when download of update resolves with failure, does not show the application window", async () => {
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: false });
const actual = getCurrentApplicationWindow();
expect(actual).toBeUndefined();
});
});
it("when process resolves without new update, does not show the application window", async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: false,
});
const actual = getCurrentApplicationWindow();
expect(actual).toBeUndefined();
});
});
describe("when user checks for updates using tray", () => {
beforeEach(() => {
builder.tray.click("check-for-updates");
});
it("user cannot check for updates again", () => {
expect(
builder.tray.get("check-for-updates")?.enabled,
).toBe(false);
});
it("name of tray item for checking updates indicates that checking is happening", () => {
expect(
builder.tray.get("check-for-updates")?.label,
).toBe("Checking for updates...");
});
it("user cannot install update yet", () => {
expect(builder.tray.get("install-update")).toBeNull();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when no new update is discovered", () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: false,
});
});
it("user cannot install update", () => {
expect(builder.tray.get("install-update")).toBeNull();
});
it("user can check for updates again", () => {
expect(
builder.tray.get("check-for-updates")?.enabled,
).toBe(true);
});
it("name of tray item for checking updates no longer indicates that checking is happening", () => {
expect(
builder.tray.get("check-for-updates")?.label,
).toBe("Check for updates");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when new update is discovered", () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
});
it("user cannot check for updates again yet", () => {
expect(
builder.tray.get("check-for-updates")?.enabled,
).toBe(false);
});
it("name of tray item for checking updates indicates that downloading is happening", () => {
expect(
builder.tray.get("check-for-updates")?.label,
).toBe("Downloading update some-version (0%)...");
});
it("when download progresses with decimals, percentage increases as integers", () => {
downloadPlatformUpdateMock.mock.calls[0][0]({ percentage: 42.424242 });
expect(
builder.tray.get("check-for-updates")?.label,
).toBe("Downloading update some-version (42%)...");
});
it("user still cannot install update", () => {
expect(builder.tray.get("install-update")).toBeNull();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when download fails", () => {
beforeEach(async () => {
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: false });
});
it("user cannot install update", () => {
expect(
builder.tray.get("install-update"),
).toBeNull();
});
it("user can check for updates again", () => {
expect(
builder.tray.get("check-for-updates")?.enabled,
).toBe(true);
});
it("name of tray item for checking updates no longer indicates that downloading is happening", () => {
expect(
builder.tray.get("check-for-updates")?.label,
).toBe("Check for updates");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when download succeeds", () => {
beforeEach(async () => {
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true });
});
it("user can install update", () => {
expect(
builder.tray.get("install-update")?.label,
).toBe("Install update some-version");
});
it("user can check for updates again", () => {
expect(
builder.tray.get("check-for-updates")?.enabled,
).toBe(true);
});
it("name of tray item for checking updates no longer indicates that downloading is happening", () => {
expect(
builder.tray.get("check-for-updates")?.label,
).toBe("Check for updates");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
});
});
});
});
@@ -0,0 +1,34 @@
/**
* 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 getTrayIconPathInjectable from "../../../../../../main/tray/menu-icon/get-tray-icon-path.injectable";
import { trayIconInjectionToken } from "../../../../../../main/tray/menu-icon/tray-icon-injection-token";
import updatesAreBeingDiscoveredInjectable from "../../../../common/updates-are-being-discovered/updates-are-being-discovered.injectable";
import updateIsBeingDownloadedInjectable from "../../../../common/update-is-being-downloaded/update-is-being-downloaded.injectable";
const checkingForUpdatesTrayIconInjectable = getInjectable({
id: "checking-for-updates-tray-icon",
instantiate: (di) => {
const getTrayIconPath = di.inject(getTrayIconPathInjectable);
const updatesAreBeingDiscovered = di.inject(updatesAreBeingDiscoveredInjectable);
const updateIsBeingDownloaded = di.inject(updateIsBeingDownloadedInjectable);
return {
iconPath: getTrayIconPath("checking-for-updates"),
priority: 1,
shouldBeShown: computed(
() =>
updatesAreBeingDiscovered.value.get() ||
updateIsBeingDownloaded.value.get(),
),
};
},
injectionToken: trayIconInjectionToken,
});
export default checkingForUpdatesTrayIconInjectable;
@@ -0,0 +1,27 @@
/**
* 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 getTrayIconPathInjectable from "../../../../../../main/tray/menu-icon/get-tray-icon-path.injectable";
import { trayIconInjectionToken } from "../../../../../../main/tray/menu-icon/tray-icon-injection-token";
import updateIsReadyToBeInstalledInjectable from "../update-is-ready-to-be-installed.injectable";
const updateIsReadyToBeInstalledTrayIconInjectable = getInjectable({
id: "update-is-ready-to-be-installed-tray-icon",
instantiate: (di) => {
const getTrayIconPath = di.inject(getTrayIconPathInjectable);
const updateIsReadyToBeInstalled = di.inject(updateIsReadyToBeInstalledInjectable);
return {
iconPath: getTrayIconPath("update-available"),
priority: 1,
shouldBeShown: updateIsReadyToBeInstalled,
};
},
injectionToken: trayIconInjectionToken,
});
export default updateIsReadyToBeInstalledTrayIconInjectable;
@@ -0,0 +1,81 @@
/**
* 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 updatingIsEnabledInjectable from "../../../../main/updating-is-enabled/updating-is-enabled.injectable";
import { trayMenuItemInjectionToken } from "../../../../../../main/tray/tray-menu-item/tray-menu-item-injection-token";
import showApplicationWindowInjectable from "../../../../../../main/start-main-application/lens-window/show-application-window.injectable";
import discoveredUpdateVersionInjectable from "../../../../common/discovered-update-version/discovered-update-version.injectable";
import updateIsBeingDownloadedInjectable from "../../../../common/update-is-being-downloaded/update-is-being-downloaded.injectable";
import updatesAreBeingDiscoveredInjectable from "../../../../common/updates-are-being-discovered/updates-are-being-discovered.injectable";
import progressOfUpdateDownloadInjectable from "../../../../common/progress-of-update-download/progress-of-update-download.injectable";
import assert from "assert";
import processCheckingForUpdatesInjectable from "../../../../main/process-checking-for-updates.injectable";
import { withErrorSuppression } from "../../../../../../common/utils/with-error-suppression/with-error-suppression";
import { pipeline } from "@ogre-tools/fp";
import withErrorLoggingInjectable from "../../../../../../common/utils/with-error-logging/with-error-logging.injectable";
const checkForUpdatesTrayItemInjectable = getInjectable({
id: "check-for-updates-tray-item",
instantiate: (di) => {
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const updatingIsEnabled = di.inject(updatingIsEnabledInjectable);
const progressOfUpdateDownload = di.inject(progressOfUpdateDownloadInjectable);
const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable);
const downloadingUpdateState = di.inject(updateIsBeingDownloadedInjectable);
const checkingForUpdatesState = di.inject(updatesAreBeingDiscoveredInjectable);
const processCheckingForUpdates = di.inject(processCheckingForUpdatesInjectable);
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
return {
id: "check-for-updates",
parentId: null,
orderNumber: 30,
label: computed(() => {
if (downloadingUpdateState.value.get()) {
const discoveredVersion = discoveredVersionState.value.get();
assert(discoveredVersion);
const roundedPercentage = Math.round(progressOfUpdateDownload.value.get().percentage);
return `Downloading update ${discoveredVersion.version} (${roundedPercentage}%)...`;
}
if (checkingForUpdatesState.value.get()) {
return "Checking for updates...";
}
return "Check for updates";
}),
enabled: computed(() => !checkingForUpdatesState.value.get() && !downloadingUpdateState.value.get()),
visible: computed(() => updatingIsEnabled),
click: pipeline(
async () => {
const { updateIsReadyToBeInstalled } = await processCheckingForUpdates("tray");
if (updateIsReadyToBeInstalled) {
await showApplicationWindow();
}
},
withErrorLoggingFor(() => "[TRAY]: Checking for updates failed."),
// TODO: Find out how to improve typing so that instead of
// x => withErrorSuppression(x) there could only be withErrorSuppression
(x) => withErrorSuppression(x),
),
};
},
injectionToken: trayMenuItemInjectionToken,
});
export default checkForUpdatesTrayItemInjectable;
@@ -0,0 +1,54 @@
/**
* 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 { trayMenuItemInjectionToken } from "../../../../../../main/tray/tray-menu-item/tray-menu-item-injection-token";
import discoveredUpdateVersionInjectable from "../../../../common/discovered-update-version/discovered-update-version.injectable";
import { withErrorSuppression } from "../../../../../../common/utils/with-error-suppression/with-error-suppression";
import { pipeline } from "@ogre-tools/fp";
import withErrorLoggingInjectable from "../../../../../../common/utils/with-error-logging/with-error-logging.injectable";
import quitAndInstallUpdateInjectable from "../../../../main/quit-and-install-update.injectable";
import updateIsReadyToBeInstalledInjectable from "../update-is-ready-to-be-installed.injectable";
const installApplicationUpdateTrayItemInjectable = getInjectable({
id: "install-update-tray-item",
instantiate: (di) => {
const quitAndInstallUpdate = di.inject(quitAndInstallUpdateInjectable);
const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable);
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
const updateIsReadyToBeInstalled = di.inject(updateIsReadyToBeInstalledInjectable);
return {
id: "install-update",
parentId: null,
orderNumber: 50,
label: computed(() => {
const versionToBeInstalled = discoveredVersionState.value.get()?.version;
return `Install update ${versionToBeInstalled}`;
}),
enabled: computed(() => true),
visible: updateIsReadyToBeInstalled,
click: pipeline(
quitAndInstallUpdate,
withErrorLoggingFor(() => "[TRAY]: Update installation failed."),
// TODO: Find out how to improve typing so that instead of
// x => withErrorSuppression(x) there could only be withErrorSuppression
(x) => withErrorSuppression(x),
),
};
},
injectionToken: trayMenuItemInjectionToken,
});
export default installApplicationUpdateTrayItemInjectable;
@@ -0,0 +1,25 @@
/**
* 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 discoveredUpdateVersionInjectable from "../../../common/discovered-update-version/discovered-update-version.injectable";
import updateIsBeingDownloadedInjectable from "../../../common/update-is-being-downloaded/update-is-being-downloaded.injectable";
const updateIsReadyToBeInstalledInjectable = getInjectable({
id: "update-is-ready-to-be-installed",
instantiate: (di) => {
const discoveredUpdateVersion = di.inject(discoveredUpdateVersionInjectable);
const updateIsBeingDownloaded = di.inject(updateIsBeingDownloadedInjectable);
return computed(
() =>
!!discoveredUpdateVersion.value.get() &&
!updateIsBeingDownloaded.value.get(),
);
},
});
export default updateIsReadyToBeInstalledInjectable;
@@ -0,0 +1,978 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`force user to update when too long since update was downloaded when application is started given checking for updates and it resolves, when update was downloaded renders 1`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="home-button"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<button
class="updateButton"
data-testid="update-button"
data-warning-level="light"
id="update-lens-button"
>
Update
<i
class="Icon icon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</button>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to some-product-name!
</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`force user to update when too long since update was downloaded when application is started given checking for updates and it resolves, when update was downloaded when enough time passes to consider that update must be installed renders 1`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="home-button"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<button
class="updateButton"
data-testid="update-button"
data-warning-level="light"
id="update-lens-button"
>
Update
<i
class="Icon icon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</button>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to some-product-name!
</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
<div
class="Animate opacity-scale Dialog flex center modal pinned enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<div
class="ForceUpdateModal"
data-testid="must-update-immediately"
>
<div
class="header"
>
<h2>
Please update
</h2>
</div>
<div
class="content"
>
<p>
An update to Lens Desktop is required to continue using the application.
</p>
</div>
<div
class="footer"
>
<button
class="Button primary"
data-testid="update-now-from-must-update-immediately-modal"
type="button"
>
Update
(
<span
data-testid="countdown-to-automatic-update"
>
5
</span>
)
</button>
</div>
</div>
</div>
</div>
</body>
`;
exports[`force user to update when too long since update was downloaded when application is started given checking for updates and it resolves, when update was downloaded when not enough time passes to consider that update must be installed renders 1`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="home-button"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<button
class="updateButton"
data-testid="update-button"
data-warning-level="light"
id="update-lens-button"
>
Update
<i
class="Icon icon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</button>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to some-product-name!
</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
@@ -0,0 +1,153 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import type { CheckForPlatformUpdates } from "../../main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "../../main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import type { DownloadPlatformUpdate } from "../../main/download-update/download-platform-update/download-platform-update.injectable";
import downloadPlatformUpdateInjectable from "../../main/download-update/download-platform-update/download-platform-update.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { DiContainer } from "@ogre-tools/injectable";
import processCheckingForUpdatesInjectable from "../../main/process-checking-for-updates.injectable";
import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import { advanceFakeTime, testUsingFakeTime } from "../../../../common/test-utils/use-fake-time";
import quitAndInstallUpdateInjectable from "../../main/quit-and-install-update.injectable";
import timeAfterUpdateMustBeInstalledInjectable from "./renderer/force-update-modal/time-after-update-must-be-installed.injectable";
import secondsAfterInstallStartsInjectable from "./renderer/force-update-modal/seconds-after-install-starts.injectable";
import forceUpdateModalRootFrameComponentInjectable from "./renderer/force-update-modal/force-update-modal-root-frame-component.injectable";
const TIME_AFTER_UPDATE_MUST_BE_INSTALLED = 1000;
const TIME_AFTER_INSTALL_STARTS = 5 * 1000;
describe("force user to update when too long since update was downloaded", () => {
let applicationBuilder: ApplicationBuilder;
let checkForPlatformUpdatesMock: AsyncFnMock<CheckForPlatformUpdates>;
let downloadPlatformUpdateMock: AsyncFnMock<DownloadPlatformUpdate>;
let mainDi: DiContainer;
let quitAndInstallUpdateMock: jest.Mock;
beforeEach(() => {
testUsingFakeTime("2015-10-21T07:28:00Z");
applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(mainDi => {
checkForPlatformUpdatesMock = asyncFn();
mainDi.override(checkForPlatformUpdatesInjectable, () => checkForPlatformUpdatesMock);
downloadPlatformUpdateMock = asyncFn();
mainDi.override(downloadPlatformUpdateInjectable, () => downloadPlatformUpdateMock);
quitAndInstallUpdateMock = jest.fn();
mainDi.override(quitAndInstallUpdateInjectable, () => quitAndInstallUpdateMock);
});
applicationBuilder.beforeWindowStart(windowDi => {
windowDi.unoverride(forceUpdateModalRootFrameComponentInjectable);
windowDi.permitSideEffects(forceUpdateModalRootFrameComponentInjectable);
windowDi.override(timeAfterUpdateMustBeInstalledInjectable, () => TIME_AFTER_UPDATE_MUST_BE_INSTALLED);
windowDi.override(secondsAfterInstallStartsInjectable, () => TIME_AFTER_INSTALL_STARTS / 1000);
});
mainDi = applicationBuilder.mainDi;
});
describe("when application is started", () => {
let rendered: RenderResult;
beforeEach(async () => {
rendered = await applicationBuilder.render();
});
describe("given checking for updates and it resolves, when update was downloaded", () => {
beforeEach(async () => {
const processCheckingForUpdates = mainDi.inject(
processCheckingForUpdatesInjectable,
);
processCheckingForUpdates("irrelevant");
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "42.0.0",
});
await downloadPlatformUpdateMock.resolve({
downloadWasSuccessful: true,
});
});
it("does not show modal yet", () => {
expect(rendered.queryByTestId("must-update-immediately")).not.toBeInTheDocument();
});
describe("when not enough time passes to consider that update must be installed", () => {
beforeEach(() => {
advanceFakeTime(TIME_AFTER_UPDATE_MUST_BE_INSTALLED - 1);
});
it("does not show modal yet", () => {
expect(rendered.queryByTestId("must-update-immediately")).not.toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when enough time passes to consider that update must be installed", () => {
beforeEach(() => {
advanceFakeTime(TIME_AFTER_UPDATE_MUST_BE_INSTALLED);
});
it("shows modal to inform about forced update", () => {
expect(rendered.getByTestId("must-update-immediately")).toBeInTheDocument();
});
it("when selected to update now, restarts the application to update", () => {
fireEvent.click(rendered.getByTestId("update-now-from-must-update-immediately-modal"));
expect(quitAndInstallUpdateMock).toHaveBeenCalled();
});
it("shows countdown for automatic update", () => {
expect(rendered.getByTestId("countdown-to-automatic-update")).toHaveTextContent("5");
});
it("when some time passes, updates the countdown for automatic update", () => {
advanceFakeTime(1000);
expect(rendered.getByTestId("countdown-to-automatic-update")).toHaveTextContent("4");
});
it("when not enough time passes for automatic update, does not restart the application yet", () => {
advanceFakeTime(TIME_AFTER_INSTALL_STARTS - 1);
expect(quitAndInstallUpdateMock).not.toHaveBeenCalled();
});
it("when enough time passes for automatically update, restarts the application to update", () => {
advanceFakeTime(TIME_AFTER_INSTALL_STARTS);
expect(quitAndInstallUpdateMock).toHaveBeenCalled();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
});
});
});
@@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { computed } from "mobx";
import { getGlobalOverride } from "../../../../../../common/test-utils/get-global-override";
import forceUpdateModalRootFrameComponentInjectable from "./force-update-modal-root-frame-component.injectable";
export default getGlobalOverride(
forceUpdateModalRootFrameComponentInjectable,
() => ({
id: "force-update-modal",
Component: () => null,
shouldRender: computed(() => false),
}),
);
@@ -0,0 +1,39 @@
/**
* 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 { rootFrameChildComponentInjectionToken } from "../../../../../../renderer/frames/root-frame/root-frame-child-component-injection-token";
import { ForceUpdateModal } from "./force-update-modal";
import timeSinceUpdateWasDownloadedInjectable from "./time-since-update-was-downloaded.injectable";
import updateDownloadedDateTimeInjectable from "../../../../common/update-downloaded-date-time/update-downloaded-date-time.injectable";
import timeAfterUpdateMustBeInstalledInjectable from "./time-after-update-must-be-installed.injectable";
const forceUpdateModalRootFrameComponentInjectable = getInjectable({
id: "force-update-modal-root-frame-component",
instantiate: (di) => {
const timeSinceUpdateWasDownloaded = di.inject(timeSinceUpdateWasDownloadedInjectable);
const updateDownloadedDateTime = di.inject(updateDownloadedDateTimeInjectable);
const timeWhenUpdateMustBeInstalled = di.inject(timeAfterUpdateMustBeInstalledInjectable);
return {
id: "force-update-modal",
Component: ForceUpdateModal,
shouldRender: computed(
() =>
!!updateDownloadedDateTime.value.get() &&
timeSinceUpdateWasDownloaded.get() >= timeWhenUpdateMustBeInstalled,
),
};
},
injectionToken: rootFrameChildComponentInjectionToken,
// Note: Globally overridden because it observes the current time which can cause long running tests
causesSideEffects: true,
});
export default forceUpdateModalRootFrameComponentInjectable;
@@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
$baseline: 8px;
.ForceUpdateModal {
background-color: white;
border-radius: 1 * $baseline;
padding: 3 * $baseline;
width: 50 * $baseline;
.header {
margin-bottom: 2 * $baseline;
}
.content {
margin-bottom: 2 * $baseline;
}
.footer {
display: flex;
justify-content: center;
}
}
@@ -0,0 +1,69 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import restartAndInstallUpdateInjectable from "../../../../renderer/restart-and-install-update.injectable";
import { Countdown } from "../../../../../../renderer/components/countdown/countdown";
import type { IComputedValue } from "mobx";
import { observer } from "mobx-react";
import installUpdateCountdownInjectable from "./install-update-countdown.injectable";
import { Dialog } from "../../../../../../renderer/components/dialog";
import { Button } from "../../../../../../renderer/components/button";
import styles from "./force-update-modal.module.scss";
interface Dependencies {
restartAndInstallUpdate: () => void;
secondsTill: IComputedValue<number>;
}
const NonInjectedForceUpdateModal = observer(
({ restartAndInstallUpdate, secondsTill }: Dependencies) => (
<Dialog isOpen={true} pinned>
<div
data-testid="must-update-immediately"
className={styles.ForceUpdateModal}
>
<div className={styles.header}>
<h2>Please update</h2>
</div>
<div className={styles.content}>
<p>
An update to Lens Desktop is required to continue using the application.
</p>
</div>
<div className={styles.footer}>
<Button
primary
data-testid="update-now-from-must-update-immediately-modal"
onClick={restartAndInstallUpdate}
label="Update"
>
{" "}
(
<Countdown
secondsTill={secondsTill}
data-testid="countdown-to-automatic-update"
/>
)
</Button>
</div>
</div>
</Dialog>
),
);
export const ForceUpdateModal = withInjectables<Dependencies>(
NonInjectedForceUpdateModal,
{
getProps: (di, props) => ({
restartAndInstallUpdate: di.inject(restartAndInstallUpdateInjectable),
secondsTill: di.inject(installUpdateCountdownInjectable),
...props,
}),
},
);
@@ -0,0 +1,27 @@
/**
* 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 countdownStateInjectable from "../../../../../../renderer/components/countdown/countdown-state.injectable";
import secondsAfterInstallStartsInjectable from "./seconds-after-install-starts.injectable";
import restartAndInstallUpdateInjectable from "../../../../renderer/restart-and-install-update.injectable";
const installUpdateCountdownInjectable = getInjectable({
id: "install-update-countdown",
instantiate: (di) => {
const secondsAfterInstallStarts = di.inject(secondsAfterInstallStartsInjectable);
const restartAndInstallUpdate = di.inject(restartAndInstallUpdateInjectable);
return di.inject(countdownStateInjectable, {
startFrom: secondsAfterInstallStarts,
onZero: () => {
restartAndInstallUpdate();
},
});
},
});
export default installUpdateCountdownInjectable;
@@ -0,0 +1,12 @@
/**
* 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";
const secondsAfterInstallStartsInjectable = getInjectable({
id: "seconds-after-install-starts",
instantiate: () => 90,
});
export default secondsAfterInstallStartsInjectable;
@@ -0,0 +1,14 @@
/**
* 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";
const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;
const timeAfterUpdateMustBeInstalledInjectable = getInjectable({
id: "time-after-update-must-be-installed",
instantiate: () => THIRTY_DAYS,
});
export default timeAfterUpdateMustBeInstalledInjectable;
@@ -0,0 +1,30 @@
/**
* 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 assert from "assert";
import { computed } from "mobx";
import moment from "moment";
import updateDownloadedDateTimeInjectable from "../../../../common/update-downloaded-date-time/update-downloaded-date-time.injectable";
import { reactiveNow } from "../../../../../../common/utils/reactive-now/reactive-now";
const timeSinceUpdateWasDownloadedInjectable = getInjectable({
id: "time-since-update-was-downloaded",
instantiate: (di) => {
const updateDownloadedDateTime = di.inject(updateDownloadedDateTimeInjectable);
return computed(() => {
const currentTimestamp = reactiveNow();
const downloadedAt = updateDownloadedDateTime.value.get();
assert(downloadedAt);
return currentTimestamp - (moment(downloadedAt).unix() * 1000);
});
},
});
export default timeSinceUpdateWasDownloadedInjectable;
@@ -0,0 +1,286 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`periodical checking of updates given updater is enabled and configuration exists, when started renders 1`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="home-button"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to some-product-name!
</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
@@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import periodicalCheckForUpdatesInjectable from "./periodical-check-for-updates.injectable";
import { getGlobalOverride } from "../../../../../common/test-utils/get-global-override";
export default getGlobalOverride(periodicalCheckForUpdatesInjectable, () => ({
start: () => {},
stop: () => {},
started: false,
}));
@@ -0,0 +1,35 @@
/**
* 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 { getStartableStoppable } from "../../../../../common/utils/get-startable-stoppable";
import processCheckingForUpdatesInjectable from "../../../main/process-checking-for-updates.injectable";
import withOrphanPromiseInjectable from "../../../../../common/utils/with-orphan-promise/with-orphan-promise.injectable";
const TWO_HOURS = 1000 * 60 * 60 * 2;
const periodicalCheckForUpdatesInjectable = getInjectable({
id: "periodical-check-for-updates",
instantiate: (di) => {
const withOrphanPromise = di.inject(withOrphanPromiseInjectable);
const processCheckingForUpdates = withOrphanPromise(di.inject(processCheckingForUpdatesInjectable));
return getStartableStoppable("periodical-check-for-updates", () => {
processCheckingForUpdates("periodic");
const intervalId = setInterval(() => {
processCheckingForUpdates("periodic");
}, TWO_HOURS);
return () => {
clearInterval(intervalId);
};
});
},
causesSideEffects: true,
});
export default periodicalCheckForUpdatesInjectable;
@@ -0,0 +1,30 @@
/**
* 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 periodicalCheckForUpdatesInjectable from "./periodical-check-for-updates.injectable";
import updatingIsEnabledInjectable from "../../../main/updating-is-enabled/updating-is-enabled.injectable";
import { afterApplicationIsLoadedInjectionToken } from "../../../../../main/start-main-application/runnable-tokens/after-application-is-loaded-injection-token";
const startCheckingForUpdatesInjectable = getInjectable({
id: "start-checking-for-updates",
instantiate: (di) => {
const periodicalCheckForUpdates = di.inject(periodicalCheckForUpdatesInjectable);
const updatingIsEnabled = di.inject(updatingIsEnabledInjectable);
return {
id: "start-checking-for-updates",
run: () => {
if (updatingIsEnabled && !periodicalCheckForUpdates.started) {
periodicalCheckForUpdates.start();
}
},
};
},
injectionToken: afterApplicationIsLoadedInjectionToken,
});
export default startCheckingForUpdatesInjectable;
@@ -0,0 +1,30 @@
/**
* 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 periodicalCheckForUpdatesInjectable from "./periodical-check-for-updates.injectable";
import { beforeQuitOfBackEndInjectionToken } from "../../../../../main/start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
const stopCheckingForUpdatesInjectable = getInjectable({
id: "stop-checking-for-updates",
instantiate: (di) => {
const periodicalCheckForUpdates = di.inject(periodicalCheckForUpdatesInjectable);
return {
id: "stop-checking-for-updates",
run: () => {
if (periodicalCheckForUpdates.started) {
periodicalCheckForUpdates.stop();
}
return undefined;
},
};
},
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopCheckingForUpdatesInjectable;
@@ -0,0 +1,116 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import type { RenderResult } from "@testing-library/react";
import electronUpdaterIsActiveInjectable from "../../../../main/electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "../../main/updating-is-enabled/publish-is-configured/publish-is-configured.injectable";
import processCheckingForUpdatesInjectable from "../../main/process-checking-for-updates.injectable";
import periodicalCheckForUpdatesInjectable from "./main/periodical-check-for-updates.injectable";
import { advanceFakeTime, testUsingFakeTime } from "../../../../common/test-utils/use-fake-time";
const ENOUGH_TIME = 1000 * 60 * 60 * 2;
describe("periodical checking of updates", () => {
let builder: ApplicationBuilder;
let processCheckingForUpdatesMock: jest.Mock;
beforeEach(() => {
testUsingFakeTime("2015-10-21T07:28:00Z");
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
mainDi.unoverride(periodicalCheckForUpdatesInjectable);
mainDi.permitSideEffects(periodicalCheckForUpdatesInjectable);
processCheckingForUpdatesMock = jest.fn();
mainDi.override(
processCheckingForUpdatesInjectable,
() => processCheckingForUpdatesMock,
);
});
});
describe("given updater is enabled and configuration exists, when started", () => {
let rendered: RenderResult;
beforeEach(async () => {
builder.beforeApplicationStart((mainDi) => {
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
});
rendered = await builder.render();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("checks for updates", () => {
expect(processCheckingForUpdatesMock).toHaveBeenCalled();
});
it("when just not enough time passes, does not check for updates again automatically yet", () => {
processCheckingForUpdatesMock.mockClear();
advanceFakeTime(ENOUGH_TIME - 1);
expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
});
it("when just enough time passes, checks for updates again automatically", () => {
processCheckingForUpdatesMock.mockClear();
advanceFakeTime(ENOUGH_TIME);
expect(processCheckingForUpdatesMock).toHaveBeenCalled();
});
});
describe("given updater is enabled but no configuration exist, when started", () => {
beforeEach(async () => {
builder.beforeApplicationStart((mainDi) => {
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => false);
});
await builder.render();
});
it("does not check for updates", () => {
expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
});
it("when time passes, never checks for updates", () => {
jest.runOnlyPendingTimers();
expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
});
});
describe("given updater is not enabled but and configuration exist, when started", () => {
beforeEach(async () => {
builder.beforeApplicationStart((mainDi) => {
mainDi.override(electronUpdaterIsActiveInjectable, () => false);
mainDi.override(publishIsConfiguredInjectable, () => true);
});
await builder.render();
});
it("does not check for updates", () => {
expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
});
it("when time passes, never checks for updates", () => {
jest.runOnlyPendingTimers();
expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
});
});
});
@@ -0,0 +1,23 @@
/**
* 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 { preferenceItemInjectionToken } from "../../../../../preferences/renderer/preference-items/preference-item-injection-token";
import { UpdateChannel } from "./update-channel";
const updateChannelPreferenceBlockInjectable = getInjectable({
id: "update-channel-preference-item",
instantiate: () => ({
kind: "block" as const,
id: "update-channel",
parentId: "application-page",
orderNumber: 50,
Component: UpdateChannel,
}),
injectionToken: preferenceItemInjectionToken,
});
export default updateChannelPreferenceBlockInjectable;
@@ -0,0 +1,51 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { SubTitle } from "../../../../../../renderer/components/layout/sub-title";
import { withInjectables } from "@ogre-tools/injectable-react";
import { Select } from "../../../../../../renderer/components/select";
import { updateChannels } from "../../../../common/update-channels";
import type { SelectedUpdateChannel } from "../../../../common/selected-update-channel/selected-update-channel.injectable";
import selectedUpdateChannelInjectable from "../../../../common/selected-update-channel/selected-update-channel.injectable";
import { pipeline } from "@ogre-tools/fp";
import { map, toPairs } from "lodash/fp";
import { observer } from "mobx-react";
interface Dependencies {
selectedUpdateChannel: SelectedUpdateChannel;
}
const updateChannelOptions = pipeline(
toPairs(updateChannels),
map(([, channel]) => ({
value: channel.id,
label: channel.label,
})),
);
const NonInjectedUpdateChannel = observer(({ selectedUpdateChannel }: Dependencies) => (
<section id="update-channel">
<SubTitle title="Update Channel" />
<Select
id="update-channel-input"
options={updateChannelOptions}
value={selectedUpdateChannel.value.get().id}
onChange={(selected) => selectedUpdateChannel.setValue(selected?.value)}
themeName="lens"
/>
</section>
));
export const UpdateChannel = withInjectables<Dependencies>(
NonInjectedUpdateChannel,
{
getProps: (di) => ({
selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable),
}),
},
);
@@ -0,0 +1,286 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`selection of update stability when started renders 1`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="home-button"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to some-product-name!
</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
@@ -0,0 +1,320 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import quitAndInstallUpdateInjectable from "../../main/quit-and-install-update.injectable";
import type { RenderResult } from "@testing-library/react";
import electronUpdaterIsActiveInjectable from "../../../../main/electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "../../main/updating-is-enabled/publish-is-configured/publish-is-configured.injectable";
import type { CheckForPlatformUpdates } from "../../main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "../../main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { UpdateChannel, ReleaseChannel } from "../../common/update-channels";
import { updateChannels } from "../../common/update-channels";
import type { DownloadPlatformUpdate } from "../../main/download-update/download-platform-update/download-platform-update.injectable";
import downloadPlatformUpdateInjectable from "../../main/download-update/download-platform-update/download-platform-update.injectable";
import selectedUpdateChannelInjectable from "../../common/selected-update-channel/selected-update-channel.injectable";
import type { IComputedValue } from "mobx";
import setUpdateOnQuitInjectable from "../../../../main/electron-app/features/set-update-on-quit.injectable";
import showInfoNotificationInjectable from "../../../../renderer/components/notifications/show-info-notification.injectable";
import processCheckingForUpdatesInjectable from "../../main/process-checking-for-updates.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import getBuildVersionInjectable
from "../../../../main/vars/build-version/get-build-version.injectable";
describe("selection of update stability", () => {
let builder: ApplicationBuilder;
let quitAndInstallUpdateMock: jest.Mock;
let checkForPlatformUpdatesMock: AsyncFnMock<CheckForPlatformUpdates>;
let downloadPlatformUpdateMock: AsyncFnMock<DownloadPlatformUpdate>;
let setUpdateOnQuitMock: jest.Mock;
let showInfoNotificationMock: jest.Mock;
let mainDi: DiContainer;
beforeEach(() => {
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
quitAndInstallUpdateMock = jest.fn();
checkForPlatformUpdatesMock = asyncFn();
downloadPlatformUpdateMock = asyncFn();
setUpdateOnQuitMock = jest.fn();
mainDi.override(setUpdateOnQuitInjectable, () => setUpdateOnQuitMock);
mainDi.override(
checkForPlatformUpdatesInjectable,
() => checkForPlatformUpdatesMock,
);
mainDi.override(
downloadPlatformUpdateInjectable,
() => downloadPlatformUpdateMock,
);
mainDi.override(
quitAndInstallUpdateInjectable,
() => quitAndInstallUpdateMock,
);
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
});
builder.beforeWindowStart((windowDi) => {
showInfoNotificationMock = jest.fn(() => () => {});
windowDi.override(showInfoNotificationInjectable, () => showInfoNotificationMock);
});
mainDi = builder.mainDi;
});
describe("when started", () => {
let rendered: RenderResult;
let processCheckingForUpdates: (source: string) => Promise<{ updateIsReadyToBeInstalled: boolean }>;
beforeEach(async () => {
rendered = await builder.render();
processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe('given update channel "alpha" is selected, when checking for updates', () => {
let selectedUpdateChannel: {
value: IComputedValue<UpdateChannel>;
setValue: (channelId: ReleaseChannel) => void;
};
beforeEach(() => {
selectedUpdateChannel = mainDi.inject(
selectedUpdateChannelInjectable,
);
selectedUpdateChannel.setValue(updateChannels.alpha.id);
processCheckingForUpdates("irrelevant");
});
it('checks updates from update channel "alpha"', () => {
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
updateChannels.alpha,
{ allowDowngrade: false },
);
});
it("when update is discovered, does not check update from other update channels", async () => {
checkForPlatformUpdatesMock.mockClear();
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
expect(checkForPlatformUpdatesMock).not.toHaveBeenCalled();
});
describe("when no update is discovered", () => {
beforeEach(async () => {
checkForPlatformUpdatesMock.mockClear();
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: false,
});
});
it('checks updates from update channel "beta"', () => {
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
updateChannels.beta,
{ allowDowngrade: false },
);
});
it("when update is discovered, does not check update from other update channels", async () => {
checkForPlatformUpdatesMock.mockClear();
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
expect(checkForPlatformUpdatesMock).not.toHaveBeenCalled();
});
describe("when no update is discovered again", () => {
beforeEach(async () => {
checkForPlatformUpdatesMock.mockClear();
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: false,
});
});
it('finally checks updates from update channel "latest"', () => {
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
updateChannels.latest,
{ allowDowngrade: false },
);
});
it("when update is discovered, does not check update from other update channels", async () => {
checkForPlatformUpdatesMock.mockClear();
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
expect(checkForPlatformUpdatesMock).not.toHaveBeenCalled();
});
});
});
});
describe('given update channel "beta" is selected', () => {
let selectedUpdateChannel: {
value: IComputedValue<UpdateChannel>;
setValue: (channelId: ReleaseChannel) => void;
};
beforeEach(() => {
selectedUpdateChannel = mainDi.inject(
selectedUpdateChannelInjectable,
);
selectedUpdateChannel.setValue(updateChannels.beta.id);
});
describe("when checking for updates", () => {
beforeEach(() => {
processCheckingForUpdates("irrelevant");
});
describe('when update from "beta" channel is discovered', () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-beta-version",
});
});
describe("when update is downloaded", () => {
beforeEach(async () => {
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true });
});
it("when user would close the application, installs the update", () => {
expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(true);
});
it('given user changes update channel to "latest", when user would close the application, does not install the update for not being stable enough', () => {
selectedUpdateChannel.setValue(updateChannels.latest.id);
expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(false);
});
it('given user changes update channel to "alpha", when user would close the application, installs the update for being stable enough', () => {
selectedUpdateChannel.setValue(updateChannels.alpha.id);
expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(true);
});
});
});
});
});
});
it("given valid update channel selection is stored, when checking for updates, checks for updates from the update channel", async () => {
await builder.render();
// TODO: Switch to more natural way of setting initial value
// TODO: UserStore is currently responsible for getting and setting initial value
mainDi.inject(selectedUpdateChannelInjectable).setValue(updateChannels.beta.id);
const processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates("irrelevant");
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.beta, expect.any(Object));
});
it("given invalid update channel selection is stored, when checking for updates, checks for updates from the update channel", async () => {
await builder.render();
// TODO: Switch to more natural way of setting initial value
// TODO: UserStore is currently responsible for getting and setting initial value
mainDi.inject(selectedUpdateChannelInjectable).setValue("something-invalid" as ReleaseChannel);
const processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates("irrelevant");
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.latest, expect.any(Object));
});
it('given no update channel selection is stored and currently using stable release, when user checks for updates, checks for updates from "latest" update channel by default', async () => {
builder.beforeApplicationStart((mainDi) => {
mainDi.override(getBuildVersionInjectable, () => () => "1.0.0");
});
await builder.render();
const processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates("irrelevant");
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
updateChannels.latest,
{ allowDowngrade: false },
);
});
it('given no update channel selection is stored and currently using alpha release, when checking for updates, checks for updates from "alpha" channel', async () => {
builder.beforeApplicationStart((mainDi) => {
mainDi.override(getBuildVersionInjectable, () => () => "1.0.0-alpha");
});
await builder.render();
const processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates("irrelevant");
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.alpha, expect.any(Object));
});
it('given no update channel selection is stored and currently using beta release, when checking for updates, checks for updates from "beta" channel', async () => {
builder.beforeApplicationStart((mainDi) => {
mainDi.override(getBuildVersionInjectable, () => () => "1.0.0-beta");
});
await builder.render();
const processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates("irrelevant");
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.beta, expect.any(Object));
});
it("given update channel selection is stored and currently using prerelease, when checking for updates, checks for updates from stored channel", async () => {
await builder.render();
// TODO: Switch to more natural way of setting initial value
// TODO: UserStore is currently responsible for getting and setting initial value
mainDi.inject(selectedUpdateChannelInjectable).setValue(updateChannels.beta.id);
const processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates("irrelevant");
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.beta, expect.any(Object));
});
});
@@ -0,0 +1,28 @@
/**
* 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 createSyncBoxInjectable from "../../../../common/utils/sync-box/create-sync-box.injectable";
import type { UpdateChannel } from "../update-channels";
import type { SyncBox } from "../../../../common/utils/sync-box/sync-box-injection-token";
import { syncBoxInjectionToken } from "../../../../common/utils/sync-box/sync-box-injection-token";
export type DiscoveredUpdateVersion = SyncBox<{ version: string; updateChannel: UpdateChannel } | null>;
const discoveredUpdateVersionInjectable = getInjectable({
id: "discovered-update-version",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox(
"discovered-update-version",
null,
) as DiscoveredUpdateVersion;
},
injectionToken: syncBoxInjectionToken,
});
export default discoveredUpdateVersionInjectable;
@@ -0,0 +1,29 @@
/**
* 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 createSyncBoxInjectable from "../../../../common/utils/sync-box/create-sync-box.injectable";
import type { SyncBox } from "../../../../common/utils/sync-box/sync-box-injection-token";
import { syncBoxInjectionToken } from "../../../../common/utils/sync-box/sync-box-injection-token";
export interface ProgressOfDownload {
percentage: number;
failed?: string;
}
export type ProgressOfUpdateDownload = SyncBox<ProgressOfDownload>;
const progressOfUpdateDownloadInjectable = getInjectable({
id: "progress-of-update-download-state",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox<ProgressOfDownload>("progress-of-update-download", { percentage: 0 });
},
injectionToken: syncBoxInjectionToken,
});
export default progressOfUpdateDownloadInjectable;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { MessageChannel } from "../../../common/utils/channel/message-channel-listener-injection-token";
export type RestartAndInstallUpdateChannel = MessageChannel<void>;
export const restartAndInstallUpdateChannel: RestartAndInstallUpdateChannel = {
id: "restart-and-install-update-channel",
};
@@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import releaseChannelInjectable from "../../../../common/vars/release-channel.injectable";
import { updateChannels } from "../update-channels";
import { createInitializableState } from "../../../../common/initializable-state/create";
const defaultUpdateChannelInjectable = createInitializableState({
id: "default-update-channel",
init: (di) => updateChannels[di.inject(releaseChannelInjectable).get()],
});
export default defaultUpdateChannelInjectable;
@@ -0,0 +1,39 @@
/**
* 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 { IComputedValue } from "mobx";
import { action, computed, observable } from "mobx";
import type { UpdateChannel, ReleaseChannel } from "../update-channels";
import { updateChannels } from "../update-channels";
import defaultUpdateChannelInjectable from "./default-update-channel.injectable";
export interface SelectedUpdateChannel {
value: IComputedValue<UpdateChannel>;
setValue: (channelId?: ReleaseChannel) => void;
}
const selectedUpdateChannelInjectable = getInjectable({
id: "selected-update-channel",
instantiate: (di): SelectedUpdateChannel => {
const defaultUpdateChannel = di.inject(defaultUpdateChannelInjectable);
const state = observable.box<UpdateChannel>(undefined, { deep: false });
return {
value: computed(() => state.get() ?? defaultUpdateChannel.get()),
setValue: action((channelId) => {
const targetUpdateChannel =
channelId && updateChannels[channelId]
? updateChannels[channelId]
: defaultUpdateChannel.get();
state.set(targetUpdateChannel);
}),
};
},
});
export default selectedUpdateChannelInjectable;
@@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type ReleaseChannel = "alpha" | "beta" | "latest";
const latestChannel: UpdateChannel = {
id: "latest",
label: "Stable",
moreStableUpdateChannel: null,
};
const betaChannel: UpdateChannel = {
id: "beta",
label: "Beta",
moreStableUpdateChannel: latestChannel,
};
const alphaChannel: UpdateChannel = {
id: "alpha",
label: "Alpha",
moreStableUpdateChannel: betaChannel,
};
export const updateChannels = {
latest: latestChannel,
beta: betaChannel,
alpha: alphaChannel,
};
export interface UpdateChannel {
readonly id: ReleaseChannel;
readonly label: string;
readonly moreStableUpdateChannel: UpdateChannel | null;
}
@@ -0,0 +1,24 @@
/**
* 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 createSyncBoxInjectable from "../../../../common/utils/sync-box/create-sync-box.injectable";
import { syncBoxInjectionToken } from "../../../../common/utils/sync-box/sync-box-injection-token";
const updateDownloadedDateTimeInjectable = getInjectable({
id: "update-downloaded-date-time",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox<string | null>(
"update-downloaded-date-time",
null,
);
},
injectionToken: syncBoxInjectionToken,
});
export default updateDownloadedDateTimeInjectable;
@@ -0,0 +1,24 @@
/**
* 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 createSyncBoxInjectable from "../../../../common/utils/sync-box/create-sync-box.injectable";
import type { SyncBox } from "../../../../common/utils/sync-box/sync-box-injection-token";
import { syncBoxInjectionToken } from "../../../../common/utils/sync-box/sync-box-injection-token";
export type UpdateIsBeingDownloaded = SyncBox<boolean>;
const updateIsBeingDownloadedInjectable = getInjectable({
id: "update-is-being-downloaded",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox("update-is-being-downloaded", false);
},
injectionToken: syncBoxInjectionToken,
});
export default updateIsBeingDownloadedInjectable;
@@ -0,0 +1,24 @@
/**
* 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 createSyncBoxInjectable from "../../../../common/utils/sync-box/create-sync-box.injectable";
import type { SyncBox } from "../../../../common/utils/sync-box/sync-box-injection-token";
import { syncBoxInjectionToken } from "../../../../common/utils/sync-box/sync-box-injection-token";
export type UpdatesAreBeingDiscovered = SyncBox<boolean>;
const updatesAreBeingDiscoveredInjectable = getInjectable({
id: "updates-are-being-discovered",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox("updates-are-being-discovered", false);
},
injectionToken: syncBoxInjectionToken,
});
export default updatesAreBeingDiscoveredInjectable;
@@ -0,0 +1,120 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "./main/updating-is-enabled/publish-is-configured/publish-is-configured.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { CheckForPlatformUpdates } from "./main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "./main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import processCheckingForUpdatesInjectable from "./main/process-checking-for-updates.injectable";
import selectedUpdateChannelInjectable from "./common/selected-update-channel/selected-update-channel.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import { updateChannels } from "./common/update-channels";
import getBuildVersionInjectable from "../../main/vars/build-version/get-build-version.injectable";
describe("downgrading version update", () => {
let applicationBuilder: ApplicationBuilder;
let checkForPlatformUpdatesMock: AsyncFnMock<CheckForPlatformUpdates>;
let mainDi: DiContainer;
beforeEach(() => {
applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(mainDi => {
checkForPlatformUpdatesMock = asyncFn();
mainDi.override(
checkForPlatformUpdatesInjectable,
() => checkForPlatformUpdatesMock,
);
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
});
mainDi = applicationBuilder.mainDi;
});
[
{
updateChannel: updateChannels.latest,
appVersion: "4.0.0",
downgradeIsAllowed: false,
},
{
updateChannel: updateChannels.beta,
appVersion: "4.0.0",
downgradeIsAllowed: false,
},
{
updateChannel: updateChannels.alpha,
appVersion: "4.0.0",
downgradeIsAllowed: false,
},
{
updateChannel: updateChannels.latest,
appVersion: "4.0.0-latest",
downgradeIsAllowed: false,
},
{
updateChannel: updateChannels.beta,
appVersion: "4.0.0-latest",
downgradeIsAllowed: false,
},
{
updateChannel: updateChannels.alpha,
appVersion: "4.0.0-latest",
downgradeIsAllowed: false,
},
{
updateChannel: updateChannels.latest,
appVersion: "4.0.0-beta",
downgradeIsAllowed: true,
},
{
updateChannel: updateChannels.beta,
appVersion: "4.0.0-beta",
downgradeIsAllowed: false,
},
{
updateChannel: updateChannels.alpha,
appVersion: "4.0.0-beta",
downgradeIsAllowed: false,
},
{
updateChannel: updateChannels.latest,
appVersion: "4.0.0-alpha",
downgradeIsAllowed: true,
},
{
updateChannel: updateChannels.beta,
appVersion: "4.0.0-alpha",
downgradeIsAllowed: false,
},
{
updateChannel: updateChannels.alpha,
appVersion: "4.0.0-alpha",
downgradeIsAllowed: false,
},
].forEach(({ appVersion, updateChannel, downgradeIsAllowed }) => {
it(`given application version "${appVersion}" and update channel "${updateChannel.id}", when checking for updates, can${downgradeIsAllowed ? "": "not"} downgrade`, async () => {
mainDi.override(getBuildVersionInjectable, () => () => appVersion);
await applicationBuilder.render();
const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable);
selectedUpdateChannel.setValue(updateChannel.id);
const processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable);
processCheckingForUpdates("irrelevant");
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(expect.any(Object), { allowDowngrade: downgradeIsAllowed });
});
});
});
@@ -0,0 +1,246 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import quitAndInstallUpdateInjectable from "./main/quit-and-install-update.injectable";
import type { RenderResult } from "@testing-library/react";
import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "./main/updating-is-enabled/publish-is-configured/publish-is-configured.injectable";
import type { CheckForPlatformUpdates } from "./main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "./main/check-for-updates/check-for-platform-updates/check-for-platform-updates.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { DownloadPlatformUpdate } from "./main/download-update/download-platform-update/download-platform-update.injectable";
import downloadPlatformUpdateInjectable from "./main/download-update/download-platform-update/download-platform-update.injectable";
import setUpdateOnQuitInjectable from "../../main/electron-app/features/set-update-on-quit.injectable";
import processCheckingForUpdatesInjectable from "./main/process-checking-for-updates.injectable";
import { testUsingFakeTime } from "../../common/test-utils/use-fake-time";
import staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable";
describe("installing update", () => {
let builder: ApplicationBuilder;
let quitAndInstallUpdateMock: jest.Mock;
let checkForPlatformUpdatesMock: AsyncFnMock<CheckForPlatformUpdates>;
let downloadPlatformUpdateMock: AsyncFnMock<DownloadPlatformUpdate>;
let setUpdateOnQuitMock: jest.Mock;
beforeEach(() => {
testUsingFakeTime("2015-10-21T07:28:00Z");
builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => {
quitAndInstallUpdateMock = jest.fn();
checkForPlatformUpdatesMock = asyncFn();
downloadPlatformUpdateMock = asyncFn();
setUpdateOnQuitMock = jest.fn();
mainDi.override(staticFilesDirectoryInjectable, () => "/some-static-files-directory");
mainDi.override(setUpdateOnQuitInjectable, () => setUpdateOnQuitMock);
mainDi.override(
checkForPlatformUpdatesInjectable,
() => checkForPlatformUpdatesMock,
);
mainDi.override(
downloadPlatformUpdateInjectable,
() => downloadPlatformUpdateMock,
);
mainDi.override(
quitAndInstallUpdateInjectable,
() => quitAndInstallUpdateMock,
);
mainDi.override(electronUpdaterIsActiveInjectable, () => true);
mainDi.override(publishIsConfiguredInjectable, () => true);
});
});
describe("when started", () => {
let rendered: RenderResult;
let processCheckingForUpdates: (source: string) => Promise<{ updateIsReadyToBeInstalled: boolean }>;
beforeEach(async () => {
rendered = await builder.render();
processCheckingForUpdates = builder.mainDi.inject(
processCheckingForUpdatesInjectable,
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows normal tray icon", () => {
expect(builder.tray.getIconPath()).toBe(
"/some-static-files-directory/build/tray/trayIconTemplate.png",
);
});
describe("when user checks for updates", () => {
beforeEach(() => {
processCheckingForUpdates("irrelevant");
});
it("checks for updates", () => {
expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
expect.any(Object),
{ allowDowngrade: false },
);
});
it("shows tray icon for checking for updates", () => {
expect(builder.tray.getIconPath()).toBe(
"/some-static-files-directory/build/tray/trayIconCheckingForUpdatesTemplate.png",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when no new update is discovered", () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: false,
});
});
it("shows tray icon for normal", () => {
expect(builder.tray.getIconPath()).toBe(
"/some-static-files-directory/build/tray/trayIconTemplate.png",
);
});
it("does not start downloading update", () => {
expect(downloadPlatformUpdateMock).not.toHaveBeenCalled();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when new update is discovered", () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
});
it("starts downloading the update", () => {
expect(downloadPlatformUpdateMock).toHaveBeenCalled();
});
it("still shows tray icon for downloading", () => {
expect(builder.tray.getIconPath()).toBe(
"/some-static-files-directory/build/tray/trayIconCheckingForUpdatesTemplate.png",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when download fails", () => {
beforeEach(async () => {
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: false });
});
it("does not quit and install update yet", () => {
expect(quitAndInstallUpdateMock).not.toHaveBeenCalled();
});
it("still shows normal tray icon", () => {
expect(builder.tray.getIconPath()).toBe(
"/some-static-files-directory/build/tray/trayIconTemplate.png",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when download succeeds", () => {
beforeEach(async () => {
await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true });
});
it("does not quit and install update yet", () => {
expect(quitAndInstallUpdateMock).not.toHaveBeenCalled();
});
it("shows tray icon for update being available", () => {
expect(builder.tray.getIconPath()).toBe(
"/some-static-files-directory/build/tray/trayIconUpdateAvailableTemplate.png",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("given checking for updates again", () => {
beforeEach(() => {
downloadPlatformUpdateMock.mockClear();
processCheckingForUpdates("irrelevant");
});
it("shows tray icon for checking for updates", () => {
expect(builder.tray.getIconPath()).toBe(
"/some-static-files-directory/build/tray/trayIconCheckingForUpdatesTemplate.png",
);
});
describe("when check resolves with same update that is already downloaded", () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-version",
});
});
it("does not re-download the update", () => {
expect(downloadPlatformUpdateMock).not.toHaveBeenCalled();
});
it("shows tray icon for update being available", () => {
expect(builder.tray.getIconPath()).toBe(
"/some-static-files-directory/build/tray/trayIconUpdateAvailableTemplate.png",
);
});
});
describe("when check resolves with different update that was previously downloaded", () => {
beforeEach(async () => {
await checkForPlatformUpdatesMock.resolve({
updateWasDiscovered: true,
version: "some-other-version",
});
});
it("downloads the update", () => {
expect(downloadPlatformUpdateMock).toHaveBeenCalled();
});
it("shows tray icon for downloading update", () => {
expect(builder.tray.getIconPath()).toBe(
"/some-static-files-directory/build/tray/trayIconCheckingForUpdatesTemplate.png",
);
});
});
});
});
});
});
});
});
@@ -0,0 +1,8 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import checkForPlatformUpdatesInjectable from "./check-for-platform-updates.injectable";
import { getGlobalOverrideForFunction } from "../../../../../common/test-utils/get-global-override-for-function";
export default getGlobalOverrideForFunction(checkForPlatformUpdatesInjectable);
@@ -0,0 +1,60 @@
/**
* 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 electronUpdaterInjectable from "../../../../../main/electron-app/features/electron-updater.injectable";
import type { UpdateChannel } from "../../../common/update-channels";
import loggerInjectable from "../../../../../common/logger.injectable";
import type { UpdateCheckResult } from "electron-updater";
export type CheckForUpdatesResult = {
updateWasDiscovered: false;
} | {
updateWasDiscovered: true;
version: string;
};
export type CheckForPlatformUpdates = (updateChannel: UpdateChannel, opts: { allowDowngrade: boolean }) => Promise<CheckForUpdatesResult>;
const checkForPlatformUpdatesInjectable = getInjectable({
id: "check-for-platform-updates",
instantiate: (di): CheckForPlatformUpdates => {
const electronUpdater = di.inject(electronUpdaterInjectable);
const logger = di.inject(loggerInjectable);
return async (updateChannel, { allowDowngrade }) => {
electronUpdater.channel = updateChannel.id;
electronUpdater.autoDownload = false;
electronUpdater.allowDowngrade = allowDowngrade;
let result: UpdateCheckResult;
try {
result = await electronUpdater.checkForUpdates();
} catch (error) {
logger.error("[UPDATE-APP/CHECK-FOR-UPDATES]", error);
return {
updateWasDiscovered: false,
};
}
const { updateInfo, cancellationToken } = result;
if (!cancellationToken) {
return {
updateWasDiscovered: false,
};
}
return {
updateWasDiscovered: true,
version: updateInfo.version,
};
};
},
});
export default checkForPlatformUpdatesInjectable;
@@ -0,0 +1,134 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../../../../../main/getDiForUnitTesting";
import electronUpdaterInjectable from "../../../../../main/electron-app/features/electron-updater.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { AppUpdater, UpdateCheckResult } from "electron-updater";
import type { CheckForPlatformUpdates } from "./check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "./check-for-platform-updates.injectable";
import type { UpdateChannel, ReleaseChannel } from "../../../common/update-channels";
import { getPromiseStatus } from "../../../../../common/test-utils/get-promise-status";
import loggerInjectable from "../../../../../common/logger.injectable";
import { noop } from "../../../../../common/utils";
describe("check-for-platform-updates", () => {
let checkForPlatformUpdates: CheckForPlatformUpdates;
let electronUpdaterFake: AppUpdater;
let checkForUpdatesMock: AsyncFnMock<() => UpdateCheckResult>;
let logErrorMock: jest.Mock;
beforeEach(() => {
const di = getDiForUnitTesting();
checkForUpdatesMock = asyncFn();
electronUpdaterFake = {
channel: undefined,
autoDownload: undefined,
allowDowngrade: undefined,
checkForUpdates: checkForUpdatesMock,
} as unknown as AppUpdater;
di.override(electronUpdaterInjectable, () => electronUpdaterFake);
logErrorMock = jest.fn();
di.override(loggerInjectable, () => ({
error: logErrorMock,
debug: noop,
info: noop,
silly: noop,
warn: noop,
}));
checkForPlatformUpdates = di.inject(checkForPlatformUpdatesInjectable);
});
describe("when called", () => {
let actualPromise: Promise<any>;
beforeEach(() => {
const testUpdateChannel: UpdateChannel = {
id: "some-update-channel" as ReleaseChannel,
label: "Some update channel",
moreStableUpdateChannel: null,
};
actualPromise = checkForPlatformUpdates(testUpdateChannel, { allowDowngrade: true });
});
it("sets update channel", () => {
expect(electronUpdaterFake.channel).toBe("some-update-channel");
});
it("sets flag for allowing downgrade", () => {
expect(electronUpdaterFake.allowDowngrade).toBe(true);
});
it("disables auto downloading for being controlled", () => {
expect(electronUpdaterFake.autoDownload).toBe(false);
});
it("checks for updates", () => {
expect(checkForUpdatesMock).toHaveBeenCalled();
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("when checking for updates resolves with update, resolves with the discovered update", async () => {
await checkForUpdatesMock.resolve({
updateInfo: {
version: "some-version",
},
cancellationToken: "some-cancellation-token",
} as unknown as UpdateCheckResult);
const actual = await actualPromise;
expect(actual).toEqual({ updateWasDiscovered: true, version: "some-version" });
});
it("when checking for updates resolves without update, resolves with update not being discovered", async () => {
await checkForUpdatesMock.resolve({
updateInfo: {
version: "some-version-that-matches-to-current-installed-version",
},
cancellationToken: null,
} as unknown as UpdateCheckResult);
const actual = await actualPromise;
expect(actual).toEqual({ updateWasDiscovered: false });
});
describe("when checking for updates rejects", () => {
let errorStub: Error;
beforeEach(() => {
errorStub = new Error("Some error");
checkForUpdatesMock.reject(errorStub);
});
it("logs errors", () => {
expect(logErrorMock).toHaveBeenCalledWith("[UPDATE-APP/CHECK-FOR-UPDATES]", errorStub);
});
it("resolves with update not being discovered", async () => {
const actual = await actualPromise;
expect(actual).toEqual({ updateWasDiscovered: false });
});
});
});
});
@@ -0,0 +1,51 @@
/**
* 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 { UpdateChannel } from "../../common/update-channels";
import checkForPlatformUpdatesInjectable from "./check-for-platform-updates/check-for-platform-updates.injectable";
import updateCanBeDowngradedInjectable from "./update-can-be-downgraded.injectable";
export type CheckForUpdatesFromChannelResult = {
updateWasDiscovered: false;
} | {
updateWasDiscovered: true;
version: string;
actualUpdateChannel: UpdateChannel;
};
const checkForUpdatesStartingFromChannelInjectable = getInjectable({
id: "check-for-updates-starting-from-channel",
instantiate: (di) => {
const checkForPlatformUpdates = di.inject(checkForPlatformUpdatesInjectable);
const updateCanBeDowngraded = di.inject(updateCanBeDowngradedInjectable);
const _recursiveCheck = async (
updateChannel: UpdateChannel,
): Promise<CheckForUpdatesFromChannelResult> => {
const result = await checkForPlatformUpdates(updateChannel, {
allowDowngrade: updateCanBeDowngraded.get(),
});
if (result.updateWasDiscovered) {
return {
updateWasDiscovered: true,
version: result.version,
actualUpdateChannel: updateChannel,
};
}
if (updateChannel.moreStableUpdateChannel) {
return await _recursiveCheck(updateChannel.moreStableUpdateChannel);
}
return { updateWasDiscovered: false };
};
return _recursiveCheck;
},
});
export default checkForUpdatesStartingFromChannelInjectable;
@@ -0,0 +1,24 @@
/**
* 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 selectedUpdateChannelInjectable from "../../common/selected-update-channel/selected-update-channel.injectable";
import releaseChannelInjectable from "../../../../common/vars/release-channel.injectable";
const updateCanBeDowngradedInjectable = getInjectable({
id: "update-can-be-downgraded",
instantiate: (di) => {
const selectedUpdateChannel = di.inject(selectedUpdateChannelInjectable);
const releaseChannel = di.inject(releaseChannelInjectable);
return computed(() => (
selectedUpdateChannel.value.get().id === "latest"
&& releaseChannel.get() !== "latest"
));
},
});
export default updateCanBeDowngradedInjectable;
Loaded 100 of 3321 files, more files were not shown because too many files have changed in this diff. Show more