Refactor and transform "Application update", "Preferences" and "Application menu" into Features (#6437)

* Move some code for application update to feature directory

The rest of the code could not be moved yet because of work-in-progress refactorings for OCP compliance.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce helper to get a global override for a function-injectable

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract global override-files from bloated getDiForUnitTesting

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Move some more code for application update to feature directory

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce competition for top bar items to achieve OCP-compliance

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract top menu item for opening context menu using the new competition

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract top menu item for navigating to home as OCP

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract top menu item for navigating to back as OCP

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract top menu item for navigating to forward as OCP

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract top menu item for application update as OCP

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Format code to make ongoing refactoring a bit easier

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Add missing unit tests for top bar extendability using extension API

This makes ongoing refactoring easier.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Replace implementation for old top-bar items for losing competition

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract top menu item for window controls as OCP

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce reusable component for spacing between other components

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce helper component to render list of React elements

Features:
- Placeholder for empty list
- Separators between items
- No boilerplate for "key" prop in React

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix double-clicking and dragging of window from top bar

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshots

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix stuff broken in rebase

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Update snapshots after rebase

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make fake time have a default value for "now"

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Migrate some application menu items to injectables

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Consolidate separators of application menu in single file

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove duplication from separators in application menu

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract some operation system actions from application menu as injectables

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract menu item for quitting application as injectable

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce way to type narrow a string property

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make the di's for unit testing able to auto-register also named exports

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make global override less strict to simplify setup of many unit tests

There's a better solution for this in the horizon, as this overridden thing is better faked than stubbed.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify a test

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce way to create hierarchical composites from a flat array

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Implement hierarchy of application menu items using "many-root" composite

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Migrate more application menu items to injectables

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Simplify hierarchy of application menu items using "single-root" composite

Also solve composed typing of application menu by using Discriminated Unions of TypeScript, see: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshot

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify creation of composite

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make composite able to have custom handler for missing parent ids

This will be useful next for application menu items, where a missing parent id cannot be fatal.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Defend against self-referencing composites

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Reintroduce non-fatal handling of orphan application menu items

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make faked, yet weak, typing a bit stronger

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify getting of composite paths

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make registrator for application menu items support all known scenarios

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Add logging for unrecognizable application menu item types

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Rename id of menu item to keep it discoverable by existing extensions

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Consolidate code to check for updates closer to feature

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce reusable horizontal line

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for preferences as a Feature

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for terminal preference tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for editor preference tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for proxy preference tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for telemetry preference tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for application preference tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move code related to helm chart preferences under related Feature in preparation for competition

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for kubernetes preference tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for application preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for editor preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for kubernetes preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for proxy preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing observer to make sure component updates

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for telemetry preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for terminal preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak UI for preferences

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove dead code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak more UI of preferences

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Revert "Introduce reusable horizontal line"

This reverts commit 4d8c147fe0f1a14bd884f73cf345e7d3a28b954a.

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove dead code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce utility to find exactly one item from array

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for preferences navigation

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move code under a Feature

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove usage of old code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove dead code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add extensions tab group to preferences

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce way to find out if composite has a descendant

This will serve eg. hiding of empty preference tab groups.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make tab groups and tabs in preferences not render when there is no content

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove code made redundant with hiding of preference tabs without content

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Tweak UI for preference navigation tab groups

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce test helper to abstract discovery of HTML elements

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Kill dead code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate discovery of HTML elements in some tests

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshots

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Adapt application builder to changes in preference navigation

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Adapt test setup to changes in preference navigation

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove uninteresting technical tests that are covered by behavioural ones

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate discovery of HTML elements in a test

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove test ID made redundant by consolidating discovery of HTML elements

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove duplication from preference pages

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix import

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make queries in element discovery return matching attribute values for easier testing

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make element discovery able to do nested discovery

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make element discovery able to discover without value for attribute

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Implement registrator for preference items

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshots

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make styling less brittle by not relying on static HTML-element structures with CSS-rules

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Add todo

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove "group" from preference types, as it is exact replica of "item"

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Update snapshots

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Replace usages of react-component factory with actual components for simplicity

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Start considering application preferences as default tab

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate naming

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make sense in horizontal lines

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Clean public interface of a normalize composite

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate name of function

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate directory structure of composite

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move utility functions to common

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Rename a preference item type to better communicate intent in UI

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move shared UI component to more common place

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate sizing of horizontal line to t-shirts

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move application update related preferences under application update feature

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make winner of competition to use original route for preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make HTML element discovery require less parameters

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract "composable-responsibilities" for Discriminable, Labelable, Orderable, and Showable

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Update snapshot

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove dead code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move code under a feature

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate navigating to preferences

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move code under a feature

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Relocate code under a sub-feature

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add TODO

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove dead code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix merge conflicts

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshots after rebase

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix import path after rebase

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove duplication from exhaustiveness checks for discriminating unions

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add TODO

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update link to a more recent article

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify static Showability of a PreferenceItem

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Move general function to general directory

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract responsibility of "separability"

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make separator in Map-component know left and right item

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove additional separators when separated items are not shown for having no content

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Update snapshots

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix lint error

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Adapt integration test to recent changes

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Adapt more integration tests to recent changes

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make composite not care about in formatting of ids

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Adapt application builder and tests to array-like paths over string-like paths

Array-like paths do not have weakness for special characters as part of id, such as ".".

Also note: the error messaging for clicking of application menu in application builder is a bit worse now I think, but the simplification of the test code is worth it in this case IMHO.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make composite not care about formatting of ids

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Adapt application builder and tests to array-like paths over string-like paths

Array-like paths do not have weakness for special characters as part of id, such as ".".

Also note: the error messaging for clicking of application menu in application builder is a bit worse now I think, but the simplification of the test code is worth it in this case IMHO.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Consolidate output of get-composite-path to match find-composite's input

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make global overrides for functions log args of the call for devability

Also make the thrown error suggest how to fix the problem.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make attempts to log error throw in unit tests

Errors cannot be allowed to happen without a unit test explicitly causing it. Errors cannot be allowed to happen without author of unit test knowing it.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make composite not know about how children are transformed

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove some duplication from tests of composite

Also make the thrown error suggest how to fix the problem.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make composite unit test an unrealistic test scenario about undefined ids

Also make the thrown error suggest how to fix the problem.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Kill dead code

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify unit tests for composite

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Consolidate tests to now point-free composite

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify "Showable"

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make non-shown application menu items not break composite structure

This was made apparent by adding related unit tests for all known environments.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify usages of Orderable and Showable

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Extract being maybe Showable as explicit composable responsibility

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make more showables maybe showable

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Consolidate logic for application menu for Windows to be based on composite

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Start using named export for composite

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Rename type for accuracy

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove unnecessary type and value

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make composable responsibilities readonly to nudge towards immutability

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Remove a bit of duplication to create TS-constants

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make a comment and test name make more sense

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify fallthrough in a switch/case

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Replace inline styles with proper CSS

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Switch to correct type to indicate "object which might not contain a property"

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify overriding of platform in a unit test

Also make typing of platforms more strict, and remove some magic strings.
Also add a TODO for further OCP-ification.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Consolidate some "maybe-types" and arguments using them

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Update snapshots

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Iku-tursoandJanne Savolainen authored and GitHub committed 2022-10-26 11:16:23 -04:00
1 parent 38feab813c
commit 37eb236948
418 files changed
+39647 -27024

No files matched your search

@@ -1,39 +0,0 @@
/**
* 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 "../../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/application-update/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;
@@ -1,25 +0,0 @@
/**
* 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;
}
}
@@ -1,69 +0,0 @@
/**
* 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 "../../components/update-button/restart-and-install-update.injectable";
import { Countdown } from "../../components/countdown/countdown";
import type { IComputedValue } from "mobx";
import { observer } from "mobx-react";
import installUpdateCountdownInjectable from "./install-update-countdown.injectable";
import { Dialog } from "../../components/dialog";
import { Button } from "../../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,
}),
},
);
@@ -1,27 +0,0 @@
/**
* 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 "../../components/countdown/countdown-state.injectable";
import secondsAfterInstallStartsInjectable from "./seconds-after-install-starts.injectable";
import restartAndInstallUpdateInjectable from "../../components/update-button/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;
@@ -1,12 +0,0 @@
/**
* 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;
@@ -1,14 +0,0 @@
/**
* 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;
@@ -1,30 +0,0 @@
/**
* 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/application-update/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;
@@ -1,14 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IComputedValue } from "mobx";
export interface AppPreferenceTabRegistration {
title: string;
id: string;
orderNumber?: number;
visible?: IComputedValue<boolean>;
}
@@ -1,21 +0,0 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import appPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/app/app-preferences-route.injectable";
import { Application } from "./application";
const appPreferencesRouteComponentInjectable = getInjectable({
id: "app-preferences-route-component",
instantiate: (di) => ({
route: di.inject(appPreferencesRouteInjectable),
Component: Application,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default appPreferencesRouteComponentInjectable;
@@ -1,23 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type React from "react";
export interface AppPreferenceComponents {
Hint: React.ComponentType<any>;
Input: React.ComponentType<any>;
}
export interface AppPreferenceRegistration {
title: string;
id?: string;
showInPreferencesTab?: string;
components: AppPreferenceComponents;
}
export interface RegisteredAppPreference extends AppPreferenceRegistration {
id: string;
}
@@ -1,19 +0,0 @@
/**
* 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 rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
import { getAppPreferences } from "./get-app-preferences";
const appPreferencesInjectable = getInjectable({
id: "app-preferences",
instantiate: (di) =>
getAppPreferences({
extensions: di.inject(rendererExtensionsInjectable),
}),
});
export default appPreferencesInjectable;
@@ -1,29 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IComputedValue } from "mobx";
import { computed } from "mobx";
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
import type { AppPreferenceRegistration, RegisteredAppPreference } from "./app-preference-registration";
interface Dependencies {
extensions: IComputedValue<LensRendererExtension[]>;
}
function getRegisteredItem(item: AppPreferenceRegistration): RegisteredAppPreference {
return {
id: item.id || item.title.toLowerCase().replace(/[^0-9a-zA-Z]+/g, "-"),
...item,
};
}
export const getAppPreferences = ({ extensions }: Dependencies) => {
return computed(() => (
extensions.get()
.flatMap((extension) => extension.appPreferences)
.map(getRegisteredItem)
));
};
@@ -1,193 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { observer } from "mobx-react";
import { SubTitle } from "../layout/sub-title";
import { Select } from "../select";
import type { ThemeStore } from "../../themes/store";
import type { UserStore } from "../../../common/user-store";
import { Input } from "../input";
import { Switch } from "../switch";
import moment from "moment-timezone";
import { defaultExtensionRegistryUrl, defaultLocaleTimezone, defaultExtensionRegistryUrlLocation } from "../../../common/user-store/preferences-helpers";
import type { IComputedValue } from "mobx";
import { runInAction } from "mobx";
import { isUrl } from "../input/input_validators";
import { ExtensionSettings } from "./extension-settings";
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
import { withInjectables } from "@ogre-tools/injectable-react";
import appPreferencesInjectable from "./app-preferences/app-preferences.injectable";
import { Preferences } from "./preferences";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import themeStoreInjectable from "../../themes/store.injectable";
import { defaultThemeId } from "../../../common/vars";
import { updateChannels } from "../../../common/application-update/update-channels";
import { map, toPairs } from "lodash/fp";
import { pipeline } from "@ogre-tools/fp";
import type { SelectedUpdateChannel } from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable";
import selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable";
interface Dependencies {
appPreferenceItems: IComputedValue<RegisteredAppPreference[]>;
userStore: UserStore;
themeStore: ThemeStore;
selectedUpdateChannel: SelectedUpdateChannel;
}
const timezoneOptions = moment.tz.names()
.map(timezone => ({
value: timezone,
label: timezone.replace("_", " "),
}));
const updateChannelOptions = pipeline(
toPairs(updateChannels),
map(([, channel]) => ({
value: channel.id,
label: channel.label,
})),
);
const extensionInstallRegistryOptions = [
{
value: "default",
label: "Default Url",
},
{
value: "npmrc",
label: "Global .npmrc file's Url",
},
{
value: "custom",
label: "Custom Url",
},
] as const;
const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, userStore, themeStore, selectedUpdateChannel }) => {
const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
const themeOptions = [
{
value: "system", // TODO: replace with a sentinal value that isn't string (and serialize it differently)
label: "Sync with computer",
},
...Array.from(themeStore.themes, ([themeId, { name }]) => ({
value: themeId,
label: name,
})),
];
const extensionSettings = appPreferenceItems.get()
.filter((preference) => preference.showInPreferencesTab === "application");
return (
<Preferences data-testid="application-preferences-page">
<section id="application">
<h2 data-testid="application-header">Application</h2>
<section id="appearance">
<SubTitle title="Theme" />
<Select
id="theme-input"
options={themeOptions}
value={userStore.colorTheme}
onChange={value => userStore.colorTheme = value?.value ?? defaultThemeId}
themeName="lens"
/>
</section>
<hr/>
<section id="extensionRegistryUrl">
<SubTitle title="Extension Install Registry" />
<Select
id="extension-install-registry-input"
options={extensionInstallRegistryOptions}
value={userStore.extensionRegistryUrl.location}
onChange={value => runInAction(() => {
userStore.extensionRegistryUrl.location = value?.value ?? defaultExtensionRegistryUrlLocation;
if (userStore.extensionRegistryUrl.location === "custom") {
userStore.extensionRegistryUrl.customUrl = "";
}
})}
themeName="lens"
/>
<p className="mt-4 mb-5 leading-relaxed">
{"This setting is to change the registry URL for installing extensions by name. "}
{`If you are unable to access the default registry (${defaultExtensionRegistryUrl}) you can change it in your `}
<b>.npmrc</b>
{" file or in the input below."}
</p>
<Input
theme="round-black"
validators={isUrl}
value={customUrl}
onChange={setCustomUrl}
onBlur={() => userStore.extensionRegistryUrl.customUrl = customUrl}
placeholder="Custom Extension Registry URL..."
disabled={userStore.extensionRegistryUrl.location !== "custom"}
/>
</section>
<hr />
<section id="other">
<SubTitle title="Start-up" />
<Switch checked={userStore.openAtLogin} onChange={() => userStore.openAtLogin = !userStore.openAtLogin}>
Automatically start Lens on login
</Switch>
</section>
<hr />
{extensionSettings.map(setting => (
<ExtensionSettings
key={setting.id}
setting={setting}
size="normal"
/>
))}
<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>
<hr />
<section id="locale">
<SubTitle title="Locale Timezone" />
<Select
id="timezone-input"
options={timezoneOptions}
value={userStore.localeTimezone}
onChange={value => userStore.localeTimezone = value?.value ?? defaultLocaleTimezone}
themeName="lens"
/>
</section>
</section>
</Preferences>
);
};
export const Application = withInjectables<Dependencies>(
observer(NonInjectedApplication),
{
getProps: (di) => ({
appPreferenceItems: di.inject(appPreferencesInjectable),
userStore: di.inject(userStoreInjectable),
themeStore: di.inject(themeStoreInjectable),
selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable),
}),
},
);
@@ -1,26 +0,0 @@
/**
* 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 observableHistoryInjectable from "../../navigation/observable-history.injectable";
import navigateToFrontPageInjectable from "../../../common/front-end-routing/navigate-to-front-page.injectable";
const closePreferencesInjectable = getInjectable({
id: "close-preferences",
instantiate: (di) => {
const observableHistory = di.inject(observableHistoryInjectable);
const navigateToFrontPage = di.inject(navigateToFrontPageInjectable);
return () => {
if (observableHistory.length <= 1) {
navigateToFrontPage();
} else {
observableHistory.goBack();
}
};
},
});
export default closePreferencesInjectable;
@@ -1,24 +0,0 @@
/**
* 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 isWindowsInjectable from "../../../common/vars/is-windows.injectable";
const defaultShellInjectable = getInjectable({
id: "default-shell",
instantiate: (di) => {
const isWindows = di.inject(isWindowsInjectable);
return (
process.env.SHELL ||
process.env.PTYSHELL ||
(isWindows ? "powershell.exe" : "System default shell")
);
},
causesSideEffects: true,
});
export default defaultShellInjectable;
@@ -1,21 +0,0 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import { Editor } from "./editor";
import editorPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/editor/editor-preferences-route.injectable";
const editorPreferencesRouteComponentInjectable = getInjectable({
id: "editor-preferences-route-component",
instantiate: (di) => ({
route: di.inject(editorPreferencesRouteInjectable),
Component: Editor,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default editorPreferencesRouteComponentInjectable;
@@ -1,127 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { observer } from "mobx-react";
import React from "react";
import type { UserStore } from "../../../common/user-store";
import { Switch } from "../switch";
import { Select } from "../select";
import { SubTitle } from "../layout/sub-title";
import { SubHeader } from "../layout/sub-header";
import { Input, InputValidators } from "../input";
import { Preferences } from "./preferences";
import { withInjectables } from "@ogre-tools/injectable-react";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import { defaultEditorConfig } from "../../../common/user-store/preferences-helpers";
import { capitalize } from "lodash";
interface Dependencies {
userStore: UserStore;
}
const minimapPositionOptions = (["left", "right"] as const)
.map(side => ({
value: side,
label: side,
}));
const lineNumberOptions = ([
"on",
"off",
"relative",
"interval",
] as const).map(lineNumbers => ({
value: lineNumbers,
label: capitalize(lineNumbers),
}));
const NonInjectedEditor = observer(({ userStore }: Dependencies) => {
const editorConfiguration = userStore.editorConfiguration;
return (
<Preferences data-testid="editor-preferences-page">
<section id="editor">
<h2 data-testid="editor-configuration-header">Editor configuration</h2>
<SubTitle title="Minimap"/>
<section>
<div className="flex gaps justify-space-between">
<div className="flex gaps align-center">
<Switch
checked={editorConfiguration.minimap.enabled}
onChange={() => editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled}
>
Show minimap
</Switch>
</div>
<div className="flex gaps align-center">
<SubHeader compact>Position</SubHeader>
<Select
id="minimap-input"
themeName="lens"
options={minimapPositionOptions}
value={editorConfiguration.minimap.side}
onChange={option => editorConfiguration.minimap.side = option?.value}
/>
</div>
</div>
</section>
<section>
<SubTitle title="Line numbers"/>
<Select
id="editor-line-numbers-input"
options={lineNumberOptions}
value={editorConfiguration.lineNumbers}
onChange={option => editorConfiguration.lineNumbers = option?.value ?? defaultEditorConfig.lineNumbers}
themeName="lens"
/>
</section>
<section>
<SubTitle title="Tab size" />
<Input
theme="round-black"
type="number"
min={1}
validators={InputValidators.isNumber}
value={editorConfiguration.tabSize.toString()}
onChange={value => editorConfiguration.tabSize = Number(value)}
/>
</section>
<section>
<SubTitle title="Font size" />
<Input
theme="round-black"
type="number"
min={10}
validators={InputValidators.isNumber}
value={editorConfiguration.fontSize.toString()}
onChange={value => editorConfiguration.fontSize = Number(value)}
/>
</section>
<section>
<SubTitle title="Font family" />
<Input
theme="round-black"
type="text"
value={editorConfiguration.fontFamily}
onChange={value => editorConfiguration.fontFamily = value}
/>
</section>
</section>
</Preferences>
);
});
export const Editor = withInjectables<Dependencies>(
NonInjectedEditor,
{
getProps: (di) => ({
userStore: di.inject(userStoreInjectable),
}),
},
);
@@ -1,52 +0,0 @@
/**
* 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 { filter, map } from "lodash/fp";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import { pipeline } from "@ogre-tools/fp";
import { extensionPreferenceItemInjectionToken } from "./extension-preference-items-injection-token";
const extensionPreferenceItemRegistratorInjectable = getInjectable({
id: "extension-preference-item-registrator",
instantiate:
() =>
(ext) => {
const extension = ext as LensRendererExtension;
return pipeline(
extension.appPreferences,
filter(
(registration) => !registration.showInPreferencesTab,
),
map((registration) => {
const id = `extension-preferences-item-${registration.id}-for-extension-${extension.sanitizedExtensionId}`;
return getInjectable({
id,
injectionToken: extensionPreferenceItemInjectionToken,
instantiate: () => ({
id: registration.id || id,
title: registration.title,
extension,
components: {
Hint: registration.components.Hint,
Input: registration.components.Input,
},
}),
});
}),
);
},
injectionToken: extensionRegistratorInjectionToken,
});
export default extensionPreferenceItemRegistratorInjectable;
@@ -1,17 +0,0 @@
/**
* 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 { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
interface ExtensionPreferenceItem extends RegisteredAppPreference {
extension: LensRendererExtension;
}
export const extensionPreferenceItemInjectionToken = getInjectionToken<ExtensionPreferenceItem>({
id: "extension-preference-item-injection-token",
});
@@ -1,23 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
export const extensionPreferenceItemInjectionToken = getInjectionToken<RegisteredAppPreference>({
id: "extension-preference-item-injection-token",
});
const extensionsPreferenceItemsInjectable = getInjectable({
id: "extension-preference-items",
instantiate: (di) => {
const computedInjectMany = di.inject(computedInjectManyInjectable);
return computedInjectMany(extensionPreferenceItemInjectionToken);
},
});
export default extensionsPreferenceItemsInjectable;
@@ -1,35 +0,0 @@
/**
* 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 extensionPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable";
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
import routePathParametersInjectable from "../../routes/route-path-parameters.injectable";
import { getExtensionPreferenceItems } from "./get-extension-preference-items";
const extensionPreferencesModelInjectable = getInjectable({
id: "extension-preferences-model",
instantiate: (di) => {
const route = di.inject(extensionPreferencesRouteInjectable);
const pathParameters = di.inject(routePathParametersInjectable, route);
const extensions = di.inject(rendererExtensionsInjectable);
return computed(() => {
const { extensionId, tabId } = pathParameters.get();
const targetExtension = extensions.get().find((extension) => extension.sanitizedExtensionId === extensionId);
const targetAppTab = targetExtension?.appPreferenceTabs.find(tab => tab.id === tabId);
const preferencePageTitle = targetAppTab?.title || `${targetExtension?.manifest.name || "Extension"} preferences`;
return {
extensionName: targetExtension?.manifest.name,
preferenceItems: getExtensionPreferenceItems(targetExtension, tabId),
preferencePageTitle,
};
});
},
});
export default extensionPreferencesModelInjectable;
@@ -1,21 +0,0 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import extensionPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable";
import { Extensions } from "./extensions";
const extensionPreferencesRouteComponentInjectable = getInjectable({
id: "extension-preferences-route-component",
instantiate: (di) => ({
route: di.inject(extensionPreferencesRouteInjectable),
Component: Extensions,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default extensionPreferencesRouteComponentInjectable;
@@ -1,39 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { SubTitle } from "../layout/sub-title";
import type { AppPreferenceRegistration } from "./app-preferences/app-preference-registration";
import type { DOMAttributes } from "react";
import React from "react";
import { cssNames } from "../../../renderer/utils";
export interface ExtensionSettingsProps extends DOMAttributes<HTMLDivElement> {
setting: AppPreferenceRegistration;
size: "small" | "normal";
}
export function ExtensionSettings({ setting, size, ...props }: ExtensionSettingsProps) {
const {
title,
id,
components: { Hint, Input },
} = setting;
return (
<React.Fragment>
<section
id={id}
className={cssNames(size)}
{...props}
>
<SubTitle title={title} />
<Input />
<div className="hint">
<Hint />
</div>
</section>
<hr className={cssNames(size)} />
</React.Fragment>
);
}
@@ -1,51 +0,0 @@
/**
* 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 { filter, map } from "lodash/fp";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import { pipeline } from "@ogre-tools/fp";
import { telemetryPreferenceItemInjectionToken } from "./telemetry-preference-items.injectable";
const extensionTelemetryPreferenceItemRegistratorInjectable = getInjectable({
id: "extension-telemetry-preference-item-registrator",
instantiate:
() =>
(ext) => {
const extension = ext as LensRendererExtension;
return pipeline(
extension.appPreferences,
filter(
(registration) => registration.showInPreferencesTab === "telemetry",
),
map((registration) => {
const id = `telemetry-preferences-item-${registration.id}-for-extension-${extension.sanitizedExtensionId}`;
return getInjectable({
id,
injectionToken: telemetryPreferenceItemInjectionToken,
instantiate: () => ({
id: registration.id || id,
title: registration.title,
components: {
Hint: registration.components.Hint,
Input: registration.components.Input,
},
}),
});
}),
);
},
injectionToken: extensionRegistratorInjectionToken,
});
export default extensionTelemetryPreferenceItemRegistratorInjectable;
@@ -1,61 +0,0 @@
/**
* 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 type { IComputedValue } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
import extensionPreferencesModelInjectable from "./extension-preference-model.injectable";
import { ExtensionSettings } from "./extension-settings";
import { Preferences } from "./preferences";
interface Dependencies {
model: IComputedValue<{
preferenceItems: RegisteredAppPreference[];
extensionName?: string;
preferencePageTitle?: string;
}>;
}
const NonInjectedExtensions = ({ model }: Dependencies) => {
const { extensionName, preferenceItems, preferencePageTitle } = model.get();
return (
<Preferences data-testid="extension-preferences-page">
<section id="extensions">
<h2 data-testid="extension-preferences-page-title">
{preferencePageTitle}
</h2>
{!extensionName && (
<div
className="flex items-center"
data-testid="error-for-extension-not-being-present"
>
No extension found
</div>
)}
{preferenceItems.map((preferenceItem, index) => (
<ExtensionSettings
key={`${preferenceItem.id}-${index}`}
setting={preferenceItem}
size="small"
data-testid={`extension-preference-item-for-${preferenceItem.id}`}
/>
))}
</section>
</Preferences>
);
};
export const Extensions = withInjectables<Dependencies>(
observer(NonInjectedExtensions),
{
getProps: (di) => ({
model: di.inject(extensionPreferencesModelInjectable),
}),
},
);
@@ -1,24 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
export function getExtensionPreferenceItems(extension?: LensRendererExtension, tabId?: string): RegisteredAppPreference[] {
if (!extension) {
return [];
}
const preferences = extension.appPreferences.map(preference => ({
id: preference.id || preference.title,
...preference,
}));
if (tabId) {
return preferences.filter(preference => preference.showInPreferencesTab == tabId);
}
return preferences.filter(preference => !preference.showInPreferencesTab);
}
@@ -1,6 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./preferences";
@@ -1,217 +0,0 @@
/**
* 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 fse from "fs-extra";
import { computed, makeObservable, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import React from "react";
import { Notice } from "../+extensions/notice";
import type { KubeconfigSyncEntry, KubeconfigSyncValue, UserStore } from "../../../common/user-store";
import { iter, tuple } from "../../utils";
import { SubTitle } from "../layout/sub-title";
import { PathPicker } from "../path-picker/path-picker";
import { Spinner } from "../spinner";
import { RemovableItem } from "./removable-item";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import type { Logger } from "../../../common/logger";
interface SyncInfo {
type: "file" | "folder" | "unknown";
}
interface Entry extends Value {
filePath: string;
}
interface Value {
data: KubeconfigSyncValue;
info: SyncInfo;
}
async function getMapEntry({ filePath, ...data }: KubeconfigSyncEntry, logger: Logger): Promise<[string, Value]> {
try {
// stat follows the stat(2) linux syscall spec, namely it follows symlinks
const stats = await fse.stat(filePath);
if (stats.isFile()) {
return [filePath, { info: { type: "file" }, data }];
}
if (stats.isDirectory()) {
return [filePath, { info: { type: "folder" }, data }];
}
logger.warn("[KubeconfigSyncs]: unknown stat entry", { stats });
return [filePath, { info: { type: "unknown" }, data }];
} catch (error) {
logger.warn(`[KubeconfigSyncs]: failed to stat entry: ${error}`, { error });
return [filePath, { info: { type: "unknown" }, data }];
}
}
export async function getAllEntries(filePaths: string[], logger: Logger): Promise<[string, Value][]> {
return Promise.all(filePaths.map(filePath => getMapEntry({ filePath }, logger)));
}
interface Dependencies {
userStore: UserStore;
isWindows: boolean;
logger: Logger;
}
@observer
class NonInjectedKubeconfigSyncs extends React.Component<Dependencies> {
syncs = observable.map<string, Value>();
@observable loaded = false;
constructor(props: Dependencies) {
super(props);
makeObservable(this);
}
async componentDidMount() {
const mapEntries = await Promise.all(
iter.map(
this.props.userStore.syncKubeconfigEntries,
([filePath, ...value]) => getMapEntry({ filePath, ...value }, this.props.logger),
),
);
this.syncs.replace(mapEntries);
this.loaded = true;
disposeOnUnmount(this, [
reaction(
() => Array.from(this.syncs.entries(), ([filePath, { data }]) => tuple.from(filePath, data)),
syncs => {
this.props.userStore.syncKubeconfigEntries.replace(syncs);
},
),
]);
}
@computed get syncsList(): Entry[] | undefined {
if (!this.loaded) {
return undefined;
}
return Array.from(this.syncs.entries(), ([filePath, value]) => ({ filePath, ...value }));
}
onPick = async (filePaths: string[]) => {
this.syncs.merge(await getAllEntries(filePaths, this.props.logger));
};
getIconName(entry: Entry) {
switch (entry.info.type) {
case "file":
return "description";
case "folder":
return "folder";
case "unknown":
return "help_outline";
}
}
renderEntry = (entry: Entry) => {
return (
<RemovableItem
key={entry.filePath}
onRemove={() => this.syncs.delete(entry.filePath)}
className="mt-3"
icon={this.getIconName(entry)}
>
<div className="flex-grow break-all">
{entry.filePath}
</div>
</RemovableItem>
);
};
renderEntries() {
const entries = this.syncsList;
if (!entries) {
return (
<div className="loading-spinner">
<Spinner />
</div>
);
}
if (!entries.length) {
return (
<Notice className="mt-3">
<div className="flex-grow text-center">No files and folders have been synced yet</div>
</Notice>
);
}
return (
<div>
{entries.map(this.renderEntry)}
</div>
);
}
renderSyncButtons() {
if (this.props.isWindows) {
return (
<div className="flex gaps align-center mb-5">
<PathPicker
label="Sync file(s)"
onPick={this.onPick}
buttonLabel="Sync"
properties={["showHiddenFiles", "multiSelections", "openFile"]}
/>
<span>or</span>
<PathPicker
label="Sync folder(s)"
onPick={this.onPick}
buttonLabel="Sync"
properties={["showHiddenFiles", "multiSelections", "openDirectory"]}
/>
</div>
);
}
return (
<div className="self-start mb-5">
<PathPicker
label="Sync Files and Folders"
onPick={this.onPick}
buttonLabel="Sync"
properties={["showHiddenFiles", "multiSelections", "openFile", "openDirectory"]}
/>
</div>
);
}
render() {
return (
<>
{this.renderSyncButtons()}
<SubTitle title="Synced Items" className="pt-5"/>
{this.renderEntries()}
</>
);
}
}
export const KubeconfigSyncs = withInjectables<Dependencies>(
NonInjectedKubeconfigSyncs,
{
getProps: (di) => ({
userStore: di.inject(userStoreInjectable),
isWindows: di.inject(isWindowsInjectable),
logger: di.inject(loggerInjectable),
}),
},
);
@@ -1,109 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React, { useState } from "react";
import { observer } from "mobx-react";
import { Input, InputValidators } from "../input";
import { SubTitle } from "../layout/sub-title";
import type { UserStore } from "../../../common/user-store";
import { Select } from "../select";
import { Switch } from "../switch";
import { defaultPackageMirror, packageMirrors } from "../../../common/user-store/preferences-helpers";
import directoryForBinariesInjectable from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import directoryForKubectlBinariesInjectable from "../../../common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable";
interface Dependencies {
defaultPathForGeneralBinaries: string;
defaultPathForKubectlBinaries: string;
userStore: UserStore;
}
const downloadMirrorOptions = Array.from(packageMirrors, ([name, mirror]) => ({
value: name,
label: mirror.label,
isDisabled: !mirror.platforms.has(process.platform),
}));
const NonInjectedKubectlBinaries= observer(({
defaultPathForGeneralBinaries,
defaultPathForKubectlBinaries,
userStore,
}: Dependencies) => {
const [downloadPath, setDownloadPath] = useState(userStore.downloadBinariesPath || "");
const [binariesPath, setBinariesPath] = useState(userStore.kubectlBinariesPath || "");
const pathValidator = downloadPath ? InputValidators.isPath : undefined;
const save = () => {
userStore.downloadBinariesPath = downloadPath;
userStore.kubectlBinariesPath = binariesPath;
};
return (
<>
<section>
<SubTitle title="Kubectl binary download" />
<Switch
checked={userStore.downloadKubectlBinaries}
onChange={() => userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries}
>
Download kubectl binaries matching the Kubernetes cluster version
</Switch>
</section>
<section>
<SubTitle title="Download mirror" />
<Select
id="download-mirror-input"
placeholder="Download mirror for kubectl"
options={downloadMirrorOptions}
value={userStore.downloadMirror}
onChange={option => userStore.downloadMirror = option?.value ?? defaultPackageMirror}
isDisabled={!userStore.downloadKubectlBinaries}
themeName="lens"
/>
</section>
<section>
<SubTitle title="Directory for binaries" />
<Input
theme="round-black"
value={downloadPath}
placeholder={defaultPathForGeneralBinaries}
validators={pathValidator}
onChange={setDownloadPath}
onBlur={save}
disabled={!userStore.downloadKubectlBinaries}
/>
<div className="hint">The directory to download binaries into.</div>
</section>
<section>
<SubTitle title="Path to kubectl binary" />
<Input
theme="round-black"
placeholder={defaultPathForKubectlBinaries}
value={binariesPath}
validators={pathValidator}
onChange={setBinariesPath}
onBlur={save}
disabled={userStore.downloadKubectlBinaries}
/>
</section>
</>
);
},
);
export const KubectlBinaries = withInjectables<Dependencies>(
NonInjectedKubectlBinaries,
{
getProps: (di) => ({
defaultPathForGeneralBinaries: di.inject(directoryForBinariesInjectable),
defaultPathForKubectlBinaries: di.inject(directoryForKubectlBinariesInjectable),
userStore: di.inject(userStoreInjectable),
}),
},
);
@@ -1,21 +0,0 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import kubernetesPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/kubernetes/kubernetes-preferences-route.injectable";
import { Kubernetes } from "./kubernetes";
const kubernetesPreferencesRouteComponentInjectable = getInjectable({
id: "kubernetes-preferences-route-component",
instantiate: (di) => ({
route: di.inject(kubernetesPreferencesRouteInjectable),
Component: Kubernetes,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default kubernetesPreferencesRouteComponentInjectable;
@@ -1,31 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { observer } from "mobx-react";
import React from "react";
import { HelmCharts } from "./kubernetes/helm-charts/helm-charts";
import { KubeconfigSyncs } from "./kubeconfig-syncs";
import { KubectlBinaries } from "./kubectl-binaries";
import { Preferences } from "./preferences";
export const Kubernetes = observer(() => (
<Preferences data-testid="kubernetes-preferences-page">
<section id="kubernetes">
<section id="kubectl">
<h2 data-testid="kubernetes-header">Kubernetes</h2>
<KubectlBinaries />
</section>
<hr />
<section id="kube-sync">
<h2 data-testid="kubernetes-sync-header">Kubeconfig Syncs</h2>
<KubeconfigSyncs />
</section>
<hr />
<section id="helm">
<h2>Helm Charts</h2>
<HelmCharts />
</section>
</section>
</Preferences>
));
@@ -1,42 +0,0 @@
/**
* 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 { asyncComputed } from "@ogre-tools/injectable-react";
import { getActiveHelmRepositoriesChannel } from "../../../../../common/helm/get-active-helm-repositories-channel";
import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token";
import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable";
import helmRepositoriesErrorStateInjectable from "./helm-repositories-error-state.injectable";
import { runInAction } from "mobx";
const activeHelmRepositoriesInjectable = getInjectable({
id: "active-helm-repositories",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const showErrorNotification = di.inject(showErrorNotificationInjectable);
const helmRepositoriesErrorState = di.inject(helmRepositoriesErrorStateInjectable);
return asyncComputed(async () => {
const result = await requestFromChannel(getActiveHelmRepositoriesChannel);
if (result.callWasSuccessful) {
return result.response;
} else {
showErrorNotification(result.error);
runInAction(() =>
helmRepositoriesErrorState.set({
controlsAreShown: false,
errorMessage: result.error,
}),
);
return [];
}
}, []);
},
});
export default activeHelmRepositoriesInjectable;
@@ -1,27 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.AddHelmRepoDialog {
--flex-gap: #{$margin * 1.5};
.accordion {
font-weight: bold;
align-self: flex-start;
padding: 0;
&:focus:not(:active) {
box-shadow: none;
}
}
.Icon {
--color-active: black;
}
.fields-title {
display: grid;
grid-template-columns: min-content auto;
align-items: center;
gap: $margin * 0.5;
}
}
@@ -1,152 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./add-helm-repo-dialog.scss";
import React from "react";
import { Wizard, WizardStep } from "../../../../wizard";
import { Input } from "../../../../input";
import { systemName, isUrl } from "../../../../input/input_validators";
import { withInjectables } from "@ogre-tools/injectable-react";
import customHelmRepoInjectable from "./custom-helm-repo.injectable";
import type { HelmRepo } from "../../../../../../common/helm/helm-repo";
import { observer } from "mobx-react";
import type { IObservableValue } from "mobx";
import { action } from "mobx";
import submitCustomHelmRepositoryInjectable from "./submit-custom-helm-repository.injectable";
import hideDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/hide-dialog-for-adding-custom-helm-repository.injectable";
import { Button } from "../../../../button";
import { Icon } from "../../../../icon";
import maximalCustomHelmRepoOptionsAreShownInjectable from "./maximal-custom-helm-repo-options-are-shown.injectable";
import { SubTitle } from "../../../../layout/sub-title";
import { Checkbox } from "../../../../checkbox";
import { HelmFileInput } from "./helm-file-input/helm-file-input";
interface Dependencies {
helmRepo: HelmRepo;
hideDialog: () => void;
submitCustomRepository: (repository: HelmRepo) => Promise<void>;
maximalOptionsAreShown: IObservableValue<boolean>;
}
const NonInjectedActivationOfCustomHelmRepositoryDialogContent = observer(({ helmRepo, submitCustomRepository, maximalOptionsAreShown, hideDialog } : Dependencies) => (
<Wizard header={<h5>Add custom Helm Repo</h5>} done={hideDialog}>
<WizardStep
contentClass="flow column"
nextLabel="Add"
next={() => submitCustomRepository(helmRepo)}
testIdForNext="custom-helm-repository-submit-button"
testIdForPrev="custom-helm-repository-cancel-button"
>
<div className="flex column gaps" data-testid="add-custom-helm-repository-dialog">
<Input
autoFocus
required
placeholder="Helm repo name"
trim
validators={systemName}
value={helmRepo.name}
onChange={action(v => helmRepo.name = v)}
data-testid="custom-helm-repository-name-input"
/>
<Input
required
placeholder="URL"
validators={isUrl}
value={helmRepo.url}
onChange={action(v => helmRepo.url = v)}
data-testid="custom-helm-repository-url-input"
/>
<Button
plain
className="accordion"
data-testid="toggle-maximal-options-for-custom-helm-repository-button"
onClick={action(() => maximalOptionsAreShown.set(!maximalOptionsAreShown.get()))}
>
More
<Icon
small
tooltip="More"
material={maximalOptionsAreShown.get() ? "remove" : "add"}
/>
</Button>
{maximalOptionsAreShown.get() && (
<div data-testid="maximal-options-for-custom-helm-repository-dialog">
<SubTitle title="Security settings" />
<Checkbox
label="Skip TLS certificate checks for the repository"
value={helmRepo.insecureSkipTlsVerify}
onChange={action(v => {
helmRepo.insecureSkipTlsVerify = v;
})}
data-testid="custom-helm-repository-verify-tls-input"
/>
<HelmFileInput
placeholder="Key file"
value={helmRepo.keyFile || ""}
setValue={action((value) => helmRepo.keyFile = value)}
fileExtensions={keyExtensions}
data-testid="custom-helm-repository-key-file-input"
/>
<HelmFileInput
placeholder="Ca file"
value={helmRepo.caFile || ""}
setValue={action((value) => helmRepo.caFile = value)}
fileExtensions={certExtensions}
data-testid="custom-helm-repository-ca-cert-file-input"
/>
<HelmFileInput
placeholder="Certificate file"
value={helmRepo.certFile || ""}
setValue={action((value) => helmRepo.certFile = value)}
fileExtensions={certExtensions}
data-testid="custom-helm-repository-cert-file-input"
/>
<SubTitle title="Chart Repository Credentials" />
<Input
placeholder="Username"
value={helmRepo.username}
onChange= {action(v => helmRepo.username = v)}
data-testid="custom-helm-repository-username-input"
/>
<Input
type="password"
placeholder="Password"
value={helmRepo.password}
onChange={action(v => helmRepo.password = v)}
data-testid="custom-helm-repository-password-input"
/>
</div>
)}
</div>
</WizardStep>
</Wizard>
));
export const AddingOfCustomHelmRepositoryDialogContent = withInjectables<Dependencies>(
NonInjectedActivationOfCustomHelmRepositoryDialogContent,
{
getProps: (di) => ({
helmRepo: di.inject(customHelmRepoInjectable),
hideDialog: di.inject(hideDialogForAddingCustomHelmRepositoryInjectable),
submitCustomRepository: di.inject(submitCustomHelmRepositoryInjectable),
maximalOptionsAreShown: di.inject(maximalCustomHelmRepoOptionsAreShownInjectable),
}),
},
);
const keyExtensions = ["key", "keystore", "jks", "p12", "pfx", "pem"];
const certExtensions = ["crt", "cer", "ca-bundle", "p7b", "p7c", "p7s", "p12", "pfx", "pem"];
@@ -1,47 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./add-helm-repo-dialog.scss";
import React from "react";
import { Dialog } from "../../../../dialog";
import { withInjectables } from "@ogre-tools/injectable-react";
import { AddingOfCustomHelmRepositoryDialogContent } from "./adding-of-custom-helm-repository-dialog-content";
import addingOfCustomHelmRepositoryDialogIsVisibleInjectable from "./dialog-visibility/adding-of-custom-helm-repository-dialog-is-visible.injectable";
import type { IObservableValue } from "mobx";
import { observer } from "mobx-react";
import hideDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/hide-dialog-for-adding-custom-helm-repository.injectable";
interface Dependencies {
contentIsVisible: IObservableValue<boolean>;
hideDialog: () => void;
}
const NonInjectedActivationOfCustomHelmRepositoryDialog = observer(({
contentIsVisible,
hideDialog,
}: Dependencies) => (
<div>
<Dialog
className="AddHelmRepoDialog"
isOpen={contentIsVisible.get()}
close={hideDialog}
>
{contentIsVisible.get() && <AddingOfCustomHelmRepositoryDialogContent />}
</Dialog>
</div>
));
export const AddingOfCustomHelmRepositoryDialog = withInjectables<Dependencies>(
NonInjectedActivationOfCustomHelmRepositoryDialog,
{
getProps: (di) => ({
contentIsVisible: di.inject(addingOfCustomHelmRepositoryDialogIsVisibleInjectable),
hideDialog: di.inject(hideDialogForAddingCustomHelmRepositoryInjectable),
}),
},
);
@@ -1,32 +0,0 @@
/**
* 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 { Button } from "../../../../button";
import showDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/show-dialog-for-adding-custom-helm-repository.injectable";
interface Dependencies {
showDialog: () => void;
}
const NonInjectedActivationOfCustomHelmRepositoryOpenButton = ({ showDialog }: Dependencies) => (
<Button
primary
label="Add Custom Helm Repo"
onClick={showDialog}
data-testid="add-custom-helm-repo-button"
/>
);
export const AddingOfCustomHelmRepositoryOpenButton = withInjectables<Dependencies>(
NonInjectedActivationOfCustomHelmRepositoryOpenButton,
{
getProps: (di) => ({
showDialog: di.inject(showDialogForAddingCustomHelmRepositoryInjectable),
}),
},
);
@@ -1,25 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { observable } from "mobx";
const customHelmRepoInjectable = getInjectable({
id: "custom-helm-repo",
instantiate: () => observable({
name: "",
url: "",
username: "",
password: "",
insecureSkipTlsVerify: false,
caFile: "",
keyFile: "",
certFile: "",
}),
lifecycle: lifecycleEnum.transient,
});
export default customHelmRepoInjectable;
@@ -1,13 +0,0 @@
/**
* 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 { observable } from "mobx";
const addingOfCustomHelmRepositoryDialogIsVisibleInjectable = getInjectable({
id: "adding-of-custom-helm-repository-dialog-is-visible",
instantiate: () => observable.box(false),
});
export default addingOfCustomHelmRepositoryDialogIsVisibleInjectable;
@@ -1,21 +0,0 @@
/**
* 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 { action } from "mobx";
import addingOfCustomHelmRepositoryDialogIsVisibleInjectable from "./adding-of-custom-helm-repository-dialog-is-visible.injectable";
const hideDialogForAddingCustomHelmRepositoryInjectable = getInjectable({
id: "hide-dialog-for-adding-custom-helm-repository",
instantiate: (di) => {
const state = di.inject(addingOfCustomHelmRepositoryDialogIsVisibleInjectable);
return action(() => {
state.set(false);
});
},
});
export default hideDialogForAddingCustomHelmRepositoryInjectable;
@@ -1,21 +0,0 @@
/**
* 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 { action } from "mobx";
import addingOfCustomHelmRepositoryDialogIsVisibleInjectable from "./adding-of-custom-helm-repository-dialog-is-visible.injectable";
const showDialogForAddingCustomHelmRepositoryInjectable = getInjectable({
id: "show-dialog-for-adding-custom-helm-repository",
instantiate: (di) => {
const state = di.inject(addingOfCustomHelmRepositoryDialogIsVisibleInjectable);
return action(() => {
state.set(true);
});
},
});
export default showDialogForAddingCustomHelmRepositoryInjectable;
@@ -1,23 +0,0 @@
/**
* 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 { FileFilter } from "electron";
import { requestOpenFilePickingDialog } from "../../../../../../ipc";
const getFilePathsInjectable = getInjectable({
id: "get-file-paths",
instantiate: () => async (fileFilter: FileFilter) =>
await requestOpenFilePickingDialog({
properties: ["openFile", "showHiddenFiles"],
message: `Select file`,
buttonLabel: `Use file`,
filters: [fileFilter, { name: "Any", extensions: ["*"] }],
}),
causesSideEffects: true,
});
export default getFilePathsInjectable;
@@ -1,74 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { InputValidator } from "../../../../../input";
import { Input } from "../../../../../input";
import { Icon } from "../../../../../icon";
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import getFilePathsInjectable from "./get-file-paths.injectable";
import type { FileFilter } from "electron";
import isPathInjectable from "../../../../../input/validators/is-path.injectable";
interface HelmFileInputProps {
placeholder: string;
fileExtensions: string[];
value: string;
setValue: (value: string) => void;
"data-testid"?: string;
}
interface Dependencies {
getFilePaths: (fileFilter: FileFilter) => Promise<{ canceled: boolean; filePaths: string[] }>;
isPath: InputValidator<true>;
}
const NonInjectedHelmFileInput = ({
placeholder,
value,
setValue,
fileExtensions,
getFilePaths,
isPath,
"data-testid": testId,
}: Dependencies & HelmFileInputProps) => (
<div className="flex gaps align-center">
<Input
placeholder={placeholder}
validators={isPath}
className="box grow"
value={value}
onChange={(v) => setValue(v)}
data-testid={testId}
/>
<Icon
material="folder"
onClick={async () => {
const { canceled, filePaths } = await getFilePaths({
name: placeholder,
extensions: fileExtensions,
});
if (!canceled && filePaths.length) {
setValue(filePaths[0]);
}
}}
tooltip="Browse"
/>
</div>
);
export const HelmFileInput = withInjectables<Dependencies, HelmFileInputProps>(
NonInjectedHelmFileInput,
{
getProps: (di, props) => ({
getFilePaths: di.inject(getFilePathsInjectable),
isPath: di.inject(isPathInjectable),
...props,
}),
},
);
@@ -1,13 +0,0 @@
/**
* 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 { observable } from "mobx";
const maximalCustomHelmRepoOptionsAreShownInjectable = getInjectable({
id: "maximal-custom-helm-repo-options-are-shown",
instantiate: () => observable.box(false),
});
export default maximalCustomHelmRepoOptionsAreShownInjectable;
@@ -1,25 +0,0 @@
/**
* 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 { HelmRepo } from "../../../../../../common/helm/helm-repo";
import addHelmRepositoryInjectable from "../adding-of-public-helm-repository/select-helm-repository/add-helm-repository.injectable";
import hideDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/hide-dialog-for-adding-custom-helm-repository.injectable";
const submitCustomHelmRepositoryInjectable = getInjectable({
id: "submit-custom-helm-repository",
instantiate: (di) => {
const addHelmRepository = di.inject(addHelmRepositoryInjectable);
const hideDialog = di.inject(hideDialogForAddingCustomHelmRepositoryInjectable);
return async (repository: HelmRepo) => {
await addHelmRepository(repository);
hideDialog();
};
},
});
export default submitCustomHelmRepositoryInjectable;
@@ -1,80 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import publicHelmRepositoriesInjectable from "./public-helm-repositories/public-helm-repositories.injectable";
import type { HelmRepo } from "../../../../../../common/helm/helm-repo";
import type { SelectOption } from "../../../../select";
import { Select } from "../../../../select";
import { Icon } from "../../../../icon";
import { observer } from "mobx-react";
import type { SingleValue } from "react-select";
import selectHelmRepositoryInjectable from "./select-helm-repository/select-helm-repository.injectable";
import { matches } from "lodash/fp";
import activeHelmRepositoriesInjectable from "../active-helm-repositories.injectable";
interface Dependencies {
publicRepositories: IAsyncComputed<HelmRepo[]>;
activeRepositories: IAsyncComputed<HelmRepo[]>;
selectRepository: (value: SingleValue<SelectOption<HelmRepo>>) => void;
}
const NonInjectedAddingOfPublicHelmRepository = observer(({
publicRepositories,
activeRepositories,
selectRepository,
}: Dependencies) => {
const dereferencesPublicRepositories = publicRepositories.value.get();
const dereferencesActiveRepositories = activeRepositories.value.get();
const valuesAreLoading = publicRepositories.pending.get() || activeRepositories.pending.get();
const repositoryOptions = dereferencesPublicRepositories.map(repository => ({
value: repository,
label: repository.name,
isSelected: !!dereferencesActiveRepositories.find(matches({ name: repository.name })),
}));
return (
<Select
id="selection-of-active-public-helm-repository"
placeholder="Repositories"
isLoading={valuesAreLoading}
isDisabled={valuesAreLoading}
options={repositoryOptions}
onChange={selectRepository}
value={dereferencesPublicRepositories}
formatOptionLabel={formatOptionLabel}
controlShouldRenderValue={false}
className="box grow"
themeName="lens"
/>
);
});
export const AddingOfPublicHelmRepository = withInjectables<Dependencies>(
NonInjectedAddingOfPublicHelmRepository,
{
getProps: (di) => ({
publicRepositories: di.inject(publicHelmRepositoriesInjectable),
activeRepositories: di.inject(activeHelmRepositoriesInjectable),
selectRepository: di.inject(selectHelmRepositoryInjectable),
}),
},
);
const formatOptionLabel = ({ value, isSelected }: SelectOption<HelmRepo>) => (
<div className="flex gaps">
<span>{value.name}</span>
{isSelected && (
<Icon
small
material="check"
className="box right" />
)}
</div>
);
@@ -1,29 +0,0 @@
/**
* 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 { sortBy } from "lodash/fp";
import type { HelmRepo } from "../../../../../../../common/helm/helm-repo";
import { customRequestPromise } from "../../../../../../../common/request";
const callForPublicHelmRepositoriesInjectable = getInjectable({
id: "call-for-public-helm-repositories",
instantiate: () => async (): Promise<HelmRepo[]> => {
const res = await customRequestPromise({
uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json",
json: true,
resolveWithFullResponse: true,
timeout: 10000,
});
const repositories = res.body as HelmRepo[];
return sortBy(repo => repo.name, repositories);
},
causesSideEffects: true,
});
export default callForPublicHelmRepositoriesInjectable;
@@ -1,21 +0,0 @@
/**
* 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 { asyncComputed } from "@ogre-tools/injectable-react";
import callForPublicHelmRepositoriesInjectable from "./call-for-public-helm-repositories.injectable";
const publicHelmRepositoriesInjectable = getInjectable({
id: "public-helm-repositories",
instantiate: (di) => {
const callForPublicHelmRepositories = di.inject(callForPublicHelmRepositoriesInjectable);
return asyncComputed(async () => {
return await callForPublicHelmRepositories();
}, []);
},
});
export default publicHelmRepositoriesInjectable;
@@ -1,41 +0,0 @@
/**
* 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 { HelmRepo } from "../../../../../../../common/helm/helm-repo";
import { requestFromChannelInjectionToken } from "../../../../../../../common/utils/channel/request-from-channel-injection-token";
import activeHelmRepositoriesInjectable from "../../active-helm-repositories.injectable";
import showErrorNotificationInjectable from "../../../../../notifications/show-error-notification.injectable";
import showSuccessNotificationInjectable from "../../../../../notifications/show-success-notification.injectable";
import { addHelmRepositoryChannel } from "../../../../../../../common/helm/add-helm-repository-channel";
const addHelmRepositoryInjectable = getInjectable({
id: "add-public-helm-repository",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable);
const showErrorNotification = di.inject(showErrorNotificationInjectable);
const showSuccessNotification = di.inject(showSuccessNotificationInjectable);
return async (repository: HelmRepo) => {
const result = await requestFromChannel(
addHelmRepositoryChannel,
repository,
);
if (result.callWasSuccessful) {
showSuccessNotification(
`Helm repository ${repository.name} has been added.`,
);
activeHelmRepositories.invalidate();
} else {
showErrorNotification(result.error);
}
};
},
});
export default addHelmRepositoryInjectable;
@@ -1,33 +0,0 @@
/**
* 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 addHelmRepositoryInjectable from "./add-helm-repository.injectable";
import type { SelectOption } from "../../../../../select";
import type { HelmRepo } from "../../../../../../../common/helm/helm-repo";
import type { SingleValue } from "react-select";
import removeHelmRepositoryInjectable from "../../remove-helm-repository.injectable";
const selectHelmRepositoryInjectable = getInjectable({
id: "select-helm-repository",
instantiate: (di) => {
const addHelmRepository = di.inject(addHelmRepositoryInjectable);
const removeHelmRepository = di.inject(removeHelmRepositoryInjectable);
return (selected: SingleValue<SelectOption<HelmRepo>>) => {
if (!selected) {
return;
}
if (!selected.isSelected) {
addHelmRepository(selected.value);
} else {
removeHelmRepository(selected.value);
}
};
},
});
export default selectHelmRepositoryInjectable;
@@ -1,27 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.repos {
display: flex;
flex-direction: column;
margin-top: calc(var(--margin) * 2);
}
.repos .repo + .repo {
margin-top: calc(var(--margin) * 1.5);
}
.contents {
display: flex;
gap: var(--padding);
}
.repoName {
font-weight: 500;
}
.repoUrl {
color: var(--textColorDimmed);
}
@@ -1,61 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { HelmRepositories } from "./helm-repositories";
import { AddingOfPublicHelmRepository } from "./adding-of-public-helm-repository/adding-of-public-helm-repository";
import { AddingOfCustomHelmRepositoryOpenButton } from "./adding-of-custom-helm-repository/adding-of-custom-helm-repository-open-button";
import { AddingOfCustomHelmRepositoryDialog } from "./adding-of-custom-helm-repository/adding-of-custom-helm-repository-dialog";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { HelmRepositoriesErrorState } from "./helm-repositories-error-state.injectable";
import helmRepositoriesErrorStateInjectable from "./helm-repositories-error-state.injectable";
import type { IObservableValue } from "mobx";
import { observer } from "mobx-react";
import { Notice } from "../../../+extensions/notice";
interface Dependencies {
helmRepositoriesErrorState: IObservableValue<HelmRepositoriesErrorState>;
}
const NonInjectedHelmCharts = observer(
({ helmRepositoriesErrorState }: Dependencies) => {
const state = helmRepositoriesErrorState.get();
return (
<div>
{!state.controlsAreShown && (
<Notice>
<div className="flex-grow text-center">{state.errorMessage}</div>
</Notice>
)}
{state.controlsAreShown && (
<div data-testid="helm-controls">
<div className="flex gaps">
<AddingOfPublicHelmRepository />
<AddingOfCustomHelmRepositoryOpenButton />
</div>
<HelmRepositories />
<AddingOfCustomHelmRepositoryDialog />
</div>
)}
</div>
);
},
);
export const HelmCharts = withInjectables<Dependencies>(
NonInjectedHelmCharts,
{
getProps: (di) => ({
helmRepositoriesErrorState: di.inject(helmRepositoriesErrorStateInjectable),
}),
},
);
@@ -1,19 +0,0 @@
/**
* 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 { observable } from "mobx";
export type HelmRepositoriesErrorState =
| { controlsAreShown: true }
| { controlsAreShown: false; errorMessage: string };
const helmRepositoriesErrorStateInjectable = getInjectable({
id: "helm-repositories-error-state",
instantiate: () =>
observable.box<HelmRepositoriesErrorState>({ controlsAreShown: true }),
});
export default helmRepositoriesErrorStateInjectable;
@@ -1,70 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./helm-charts.module.scss";
import React from "react";
import { observer } from "mobx-react";
import activeHelmRepositoriesInjectable from "./active-helm-repositories.injectable";
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import { Spinner } from "../../../spinner";
import type { HelmRepo } from "../../../../../common/helm/helm-repo";
import { RemovableItem } from "../../removable-item";
import removeHelmRepositoryInjectable from "./remove-helm-repository.injectable";
interface Dependencies {
activeHelmRepositories: IAsyncComputed<HelmRepo[]>;
removeRepository: (repository: HelmRepo) => Promise<void>;
}
const NonInjectedActiveHelmRepositories = observer(({ activeHelmRepositories, removeRepository }: Dependencies) => {
if (activeHelmRepositories.pending.get()) {
return (
<div className={styles.repos}>
<div className="pt-5 relative">
<Spinner center data-testid="helm-repositories-are-loading" />
</div>
</div>
);
}
const repositories = activeHelmRepositories.value.get();
return (
<div className={styles.repos}>
{repositories.map((repository) => (
<RemovableItem
key={repository.name}
onRemove={() => removeRepository(repository)}
className={styles.repo}
data-testid={`remove-helm-repository-${repository.name}`}
>
<div>
<div data-testid={`helm-repository-${repository.name}`} className={styles.repoName}>
{repository.name}
</div>
<div className={styles.repoUrl}>{repository.url}</div>
</div>
</RemovableItem>
))}
</div>
);
});
export const HelmRepositories = withInjectables<Dependencies>(
NonInjectedActiveHelmRepositories,
{
getProps: (di) => ({
activeHelmRepositories: di.inject(activeHelmRepositoriesInjectable),
removeRepository: di.inject(removeHelmRepositoryInjectable),
}),
},
);
@@ -1,26 +0,0 @@
/**
* 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 { HelmRepo } from "../../../../../common/helm/helm-repo";
import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token";
import activeHelmRepositoriesInjectable from "./active-helm-repositories.injectable";
import { removeHelmRepositoryChannel } from "../../../../../common/helm/remove-helm-repository-channel";
const removePublicHelmRepositoryInjectable = getInjectable({
id: "remove-public-helm-repository",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable);
return async (repository: HelmRepo) => {
await requestFromChannel(removeHelmRepositoryChannel, repository);
activeHelmRepositories.invalidate();
};
},
});
export default removePublicHelmRepositoryInjectable;
@@ -1,148 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "@testing-library/jest-dom/extend-expect";
import type { RenderResult } from "@testing-library/react";
import { computed } from "mobx";
import { noop } from "../../../../utils";
import type { ApplicationBuilder } from "../../../test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../test-utils/get-application-builder";
import type { PreferenceNavigationItem } from "../preference-navigation-items.injectable";
import preferenceNavigationItemsInjectable from "../preference-navigation-items.injectable";
describe.only("preferences - navigation block with links", () => {
let builder: ApplicationBuilder;
beforeEach(() => {
builder = getApplicationBuilder();
});
describe("given in preferences, when rendered", () => {
let renderer: RenderResult;
describe("when general navigation items passed", () => {
beforeEach(async () => {
builder.beforeWindowStart((windowDi) => {
windowDi.override(preferenceNavigationItemsInjectable, () =>
computed(() => generalNavItems),
);
builder.preferences.navigate();
});
renderer = await builder.render();
});
const links = ["General", "Proxy"];
it.each(links)("renders link with text content %s", (link) => {
expect(renderer.container).toHaveTextContent(link);
});
it("does not show custom settings block", () => {
expect(
renderer.queryByTestId("extension-settings"),
).not.toBeInTheDocument();
});
});
describe("when general + extension navigation items passed", () => {
beforeEach(async () => {
builder.beforeWindowStart((windowDi) => {
windowDi.override(preferenceNavigationItemsInjectable, () =>
computed(() => [...generalNavItems, ...extensionNavItems]),
);
builder.preferences.navigate();
});
renderer = await builder.render();
});
const generalLinks = ["General", "Proxy"];
it.each(generalLinks)(
"renders general link with text content %s",
(link) => {
expect(renderer.container).toHaveTextContent(link);
},
);
it("shows custom settings block", () => {
expect(
renderer.queryByTestId("extension-settings"),
).toBeInTheDocument();
});
const extensionLinks = ["lensapp-node-menu", "lensapp-pod-menu"];
it.each(extensionLinks)("shows extension navigation item %s", (link) => {
expect(
renderer.getByTestId(
`tab-link-for-extension-preferences-navigation-item-${link}`,
),
).toBeInTheDocument();
});
it("renders extension navigation items inside custom settings block", () => {
const settingsBlock = renderer.getByTestId("extension-settings");
expect(settingsBlock).toHaveTextContent("lensapp-node-menu");
});
});
});
});
const generalNavItems: PreferenceNavigationItem[] = [
{
id: "general",
label: "General",
isActive: computed(() => false),
isVisible: computed(() => true),
navigate: () => noop,
orderNumber: 0,
parent: "general",
},
{
id: "proxy",
label: "Proxy",
isActive: computed(() => false),
isVisible: computed(() => true),
navigate: () => noop,
orderNumber: 1,
parent: "general",
},
];
const extensionNavItems = [
{
id: "extension-preferences-navigation-item-lensapp-node-menu",
label: "lensapp-node-menu",
isActive: computed(() => false),
isVisible: computed(() => true),
navigate: () => noop,
orderNumber: 0,
parent: "extensions",
},
{
id: "extension-preferences-navigation-item-lensapp-pod-menu",
label: "lensapp-pod-menu",
isActive: computed(() => false),
isVisible: computed(() => true),
navigate: () => noop,
orderNumber: 0,
parent: "extensions",
},
{
id: "extension-preferences-navigation-item-metrics-plugin",
label: "metrics-plugin",
isActive: computed(() => false),
isVisible: computed(() => false),
navigate: () => noop,
orderNumber: 0,
parent: "extensions",
},
];
@@ -1,38 +0,0 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import appPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/app/app-preferences-route.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const applicationPreferencesNavigationItemInjectable = getInjectable({
id: "application-preferences-navigation-item",
instantiate: (di) => {
const route = di.inject(appPreferencesRouteInjectable);
const navigateToPreferenceTab = di.inject(navigateToPreferenceTabInjectable);
const routeIsActive = di.inject(
routeIsActiveInjectable,
route,
);
return {
id: "application",
label: "App",
parent: "general",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(() => true),
orderNumber: 10,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default applicationPreferencesNavigationItemInjectable;
@@ -1,38 +0,0 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import editorPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/editor/editor-preferences-route.injectable";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const editorPreferencesNavigationItemInjectable = getInjectable({
id: "editor-preferences-navigation-item",
instantiate: (di) => {
const route = di.inject(editorPreferencesRouteInjectable);
const navigateToPreferenceTab = di.inject(navigateToPreferenceTabInjectable);
const routeIsActive = di.inject(
routeIsActiveInjectable,
route,
);
return {
id: "editor",
label: "Editor",
parent: "general",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(() => true),
orderNumber: 40,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default editorPreferencesNavigationItemInjectable;
@@ -1,68 +0,0 @@
/**
* 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 { map } from "lodash/fp";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
import { extensionRegistratorInjectionToken } from "../../../../extensions/extension-loader/extension-registrator-injection-token";
import { pipeline } from "@ogre-tools/fp";
import extensionPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable";
import navigateToExtensionPreferencesInjectable from "../../../../common/front-end-routing/routes/preferences/extension/navigate-to-extension-preferences.injectable";
import type { LensExtension } from "../../../../extensions/lens-extension";
import routePathParametersInjectable from "../../../routes/route-path-parameters.injectable";
const extensionSpecificTabNavigationItemRegistratorInjectable = getInjectable({
id: "extension-specific-tab-preferences-navigation-items",
instantiate: (di) => {
return (ext: LensExtension) => {
const extension = ext as LensRendererExtension;
const navigateToExtensionPreferences = di.inject(
navigateToExtensionPreferencesInjectable,
);
const route = di.inject(extensionPreferencesRouteInjectable);
const routeIsActive = di.inject(routeIsActiveInjectable, route);
const pathParameters = di.inject(routePathParametersInjectable, route);
return pipeline(
extension.appPreferenceTabs,
map((tab) => {
const id = `extension-${extension.sanitizedExtensionId}-nav-item-${tab.id}`;
const isActive = computed(() => routeIsActive.get() && pathParameters.get().tabId === tab.id);
return getInjectable({
id,
injectionToken: preferenceNavigationItemInjectionToken,
instantiate: () => ({
id,
label: tab.title,
parent: "general",
orderNumber: tab.orderNumber || 100,
navigate: () => navigateToExtensionPreferences(extension.sanitizedExtensionId, tab.id),
isVisible: computed(() => {
if (!tab.visible) {
return true;
}
return tab.visible.get();
}),
isActive,
}),
});
}),
);
};
},
injectionToken: extensionRegistratorInjectionToken,
});
export default extensionSpecificTabNavigationItemRegistratorInjectable;
@@ -1,56 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import type { PreferenceNavigationItem } from "./preference-navigation-items.injectable";
import { Icon } from "../../icon";
import { PreferencesNavigationTab } from "./preference-navigation-tab";
import preferenceNavigationItemsForGroupInjectable from "./preference-navigation-items-for-group.injectable";
import { observer } from "mobx-react";
interface Dependencies {
navigationItems: IComputedValue<PreferenceNavigationItem[]>;
}
const NonInjectedExtensionsNavGroup = observer((props: Dependencies) => {
if (!props.navigationItems.get().length) {
return null;
}
return (
<div data-testid="extension-settings">
<hr/>
<div className="header flex items-center">
<Icon
material="extension"
smallest
className="mr-3"
/>
{" "}
Extensions
</div>
<div>
{props.navigationItems.get().map(item => (
<PreferencesNavigationTab
key={item.id}
item={item}
data-testid={`tab-link-for-${item.id}`}
/>
))}
</div>
</div>
);
});
export const ExtensionsNavGroup = withInjectables<Dependencies>(
NonInjectedExtensionsNavGroup,
{
getProps: (di) => ({
navigationItems: di.inject(preferenceNavigationItemsForGroupInjectable, "extensions"),
}),
},
);
@@ -1,57 +0,0 @@
/**
* 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 extensionPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable";
import navigateToExtensionPreferencesInjectable from "../../../../common/front-end-routing/routes/preferences/extension/navigate-to-extension-preferences.injectable";
import { extensionRegistratorInjectionToken } from "../../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import routePathParametersInjectable from "../../../routes/route-path-parameters.injectable";
import { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
const extensionPreferencesNavigationItemRegistratorInjectable = getInjectable({
id: "extension-preferences-navigation-item",
instantiate: (di) => {
return (ext) => {
const extension = ext as LensRendererExtension;
const navigateToExtensionPreferences = di.inject(
navigateToExtensionPreferencesInjectable,
);
const extensionHasPreferences = extension.appPreferences.length > 0;
const extensionHasGeneralPreferences = extension.appPreferences.some(preferences =>
!preferences.showInPreferencesTab,
);
const isVisible = computed(() => extensionHasPreferences && extensionHasGeneralPreferences);
const extensionRoute = di.inject(extensionPreferencesRouteInjectable);
const pathParameters = di.inject(routePathParametersInjectable, extensionRoute);
const routeIsActive = di.inject(routeIsActiveInjectable, extensionRoute);
const isActive = computed(() => routeIsActive.get() && pathParameters.get().extensionId === extension.sanitizedExtensionId);
const id = `extension-preferences-navigation-item-${extension.sanitizedExtensionId}`;
const injectable = getInjectable({
id,
injectionToken: preferenceNavigationItemInjectionToken,
instantiate: () => ({
id: `extension-${extension.sanitizedExtensionId}`,
label: `${extension.name}`,
navigate: () => navigateToExtensionPreferences(extension.sanitizedExtensionId),
isActive,
isVisible,
orderNumber: 20,
parent: "extensions",
}),
});
return [injectable];
};
},
injectionToken: extensionRegistratorInjectionToken,
});
export default extensionPreferencesNavigationItemRegistratorInjectable;
@@ -1,45 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import type { PreferenceNavigationItem } from "./preference-navigation-items.injectable";
import { PreferencesNavigationTab } from "./preference-navigation-tab";
import preferenceNavigationItemsForGroupInjectable from "./preference-navigation-items-for-group.injectable";
import { observer } from "mobx-react";
interface Dependencies {
navigationItems: IComputedValue<PreferenceNavigationItem[]>;
}
const NonInjectedGeneralNavGroup = observer((props: Dependencies) => {
if (!props.navigationItems.get().length) {
return null;
}
return (
<React.Fragment>
<div className="header">Preferences</div>
{props.navigationItems.get().map(item => (
<PreferencesNavigationTab
key={item.id}
item={item}
data-testid={`tab-link-for-${item.id}`}
/>
))}
</React.Fragment>
);
});
export const GeneralNavGroup = withInjectables<Dependencies>(
NonInjectedGeneralNavGroup,
{
getProps: (di) => ({
navigationItems: di.inject(preferenceNavigationItemsForGroupInjectable, "general"),
}),
},
);
@@ -1,41 +0,0 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import kubernetesPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/kubernetes/kubernetes-preferences-route.injectable";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const kubernetesPreferencesNavigationItemInjectable = getInjectable({
id: "kubernetes-preferences-navigation-item",
instantiate: (di) => {
const navigateToPreferenceTab = di.inject(navigateToPreferenceTabInjectable);
const route = di.inject(
kubernetesPreferencesRouteInjectable,
);
const routeIsActive = di.inject(
routeIsActiveInjectable,
route,
);
return {
id: "kubernetes",
label: "Kubernetes",
parent: "general",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(() => true),
orderNumber: 30,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default kubernetesPreferencesNavigationItemInjectable;
@@ -1,21 +0,0 @@
/**
* 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 { Route } from "../../../../common/front-end-routing/front-end-route-injection-token";
import { navigateToRouteInjectionToken } from "../../../../common/front-end-routing/navigate-to-route-injection-token";
const navigateToPreferenceTabInjectable = getInjectable({
id: "navigate-to-preference-tab",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
return (route: Route) => () => {
navigateToRoute(route, { withoutAffectingBackButton: true });
};
},
});
export default navigateToPreferenceTabInjectable;
@@ -1,30 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
getInjectable,
lifecycleEnum,
} from "@ogre-tools/injectable";
import { computed } from "mobx";
import type { PreferenceNavigationItem } from "./preference-navigation-items.injectable";
import preferenceNavigationItemsInjectable from "./preference-navigation-items.injectable";
const preferenceNavigationItemsForGroupInjectable = getInjectable({
id: "preference-navigation-items-for-group",
instantiate: (di, group: string) => {
const preferenceNavigationItems = di.inject(preferenceNavigationItemsInjectable);
return computed((): PreferenceNavigationItem[] =>
preferenceNavigationItems.get().filter((item) => item.parent == group),
);
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, group: string) => group,
}),
});
export default preferenceNavigationItemsForGroupInjectable;
@@ -1,44 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
import { filter, orderBy } from "lodash/fp";
import type { IComputedValue } from "mobx";
import { computed } from "mobx";
export const preferenceNavigationItemInjectionToken =
getInjectionToken<PreferenceNavigationItem>({
id: "preference-navigation-item-injection-token",
});
export interface PreferenceNavigationItem {
id: string;
label: string;
isActive: IComputedValue<boolean>;
isVisible: IComputedValue<boolean>;
navigate: () => void;
orderNumber: number;
parent: string;
}
const preferenceNavigationItemsInjectable = getInjectable({
id: "preference-navigation-items",
instantiate: (di) => {
const computedInjectMany = di.inject(computedInjectManyInjectable);
const navigationItems = computedInjectMany(preferenceNavigationItemInjectionToken);
return computed((): PreferenceNavigationItem[] =>
pipeline(
navigationItems.get(),
filter((item) => !!item.isVisible.get()),
(items) => orderBy([(item) => item.orderNumber], ["asc"], items),
),
);
},
});
export default preferenceNavigationItemsInjectable;
@@ -1,21 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { observer } from "mobx-react";
import React from "react";
import { Tab } from "../../tabs";
import type { PreferenceNavigationItem } from "./preference-navigation-items.injectable";
interface PreferenceNavigationTabProps extends React.DOMAttributes<HTMLElement> {
item: PreferenceNavigationItem;
}
export const PreferencesNavigationTab = observer(({ item }: PreferenceNavigationTabProps) => (
<Tab
value={item}
label={item.label}
data-testid={`tab-link-for-${item.id}`}
active={item.isActive.get()}
/>
));
@@ -1,24 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { Tabs } from "../../tabs";
import { ExtensionsNavGroup } from "./extensions-nav-group";
import { GeneralNavGroup } from "./general-nav-group";
import type {
PreferenceNavigationItem,
} from "./preference-navigation-items.injectable";
export const PreferencesNavigation = () => {
return (
<Tabs
className="flex column"
scrollable={false}
onChange={(item: PreferenceNavigationItem) => item.navigate()}
>
<GeneralNavGroup/>
<ExtensionsNavGroup/>
</Tabs>
);
};
@@ -1,38 +0,0 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import proxyPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/proxy/proxy-preferences-route.injectable";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const proxyPreferencesNavigationItemInjectable = getInjectable({
id: "proxy-preferences-navigation-item",
instantiate: (di) => {
const route = di.inject(proxyPreferencesRouteInjectable);
const navigateToPreferenceTab = di.inject(
navigateToPreferenceTabInjectable,
);
const routeIsActive = di.inject(routeIsActiveInjectable, route);
return {
id: "proxy",
label: "Proxy",
parent: "general",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(() => true),
orderNumber: 20,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default proxyPreferencesNavigationItemInjectable;
@@ -1,50 +0,0 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import telemetryPreferenceItemsInjectable from "../telemetry-preference-items.injectable";
import telemetryPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/telemetry/telemetry-preferences-route.injectable";
import sentryDataSourceNameInjectable from "../../../../common/vars/sentry-dsn-url.injectable";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const terminalPreferencesNavigationItemInjectable = getInjectable({
id: "telemetry-preferences-navigation-item",
instantiate: (di) => {
const sentryDataSourceName = di.inject(sentryDataSourceNameInjectable);
const telemetryPreferenceItems = di.inject(
telemetryPreferenceItemsInjectable,
);
const navigateToPreferenceTab = di.inject(
navigateToPreferenceTabInjectable,
);
const route = di.inject(telemetryPreferencesRouteInjectable);
const routeIsActive = di.inject(routeIsActiveInjectable, route);
return {
id: "telemetry",
label: "Telemetry",
parent: "general",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(
() => !!sentryDataSourceName || telemetryPreferenceItems.get().length > 0,
),
orderNumber: 60,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default terminalPreferencesNavigationItemInjectable;
@@ -1,38 +0,0 @@
/**
* 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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable";
import routeIsActiveInjectable from "../../../routes/route-is-active.injectable";
import { computed } from "mobx";
import terminalPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/terminal/terminal-preferences-route.injectable";
import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable";
const terminalPreferencesNavigationItemInjectable = getInjectable({
id: "terminal-preferences-navigation-item",
instantiate: (di) => {
const navigateToPreferenceTab = di.inject(
navigateToPreferenceTabInjectable,
);
const route = di.inject(terminalPreferencesRouteInjectable);
const routeIsActive = di.inject(routeIsActiveInjectable, route);
return {
id: "terminal",
label: "Terminal",
parent: "general",
navigate: navigateToPreferenceTab(route),
isActive: routeIsActive,
isVisible: computed(() => true),
orderNumber: 50,
};
},
injectionToken: preferenceNavigationItemInjectionToken,
});
export default terminalPreferencesNavigationItemInjectable;
@@ -1,10 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.Preferences {
.loading-spinner {
margin: auto;
}
}
@@ -1,48 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./preferences.scss";
import React from "react";
import { SettingLayout } from "../layout/setting-layout";
import { PreferencesNavigation } from "./preferences-navigation/preferences-navigation";
import { withInjectables } from "@ogre-tools/injectable-react";
import closePreferencesInjectable from "./close-preferences.injectable";
interface PreferencesProps extends React.DOMAttributes<any> {
children: React.ReactNode;
}
interface Dependencies {
closePreferences: () => void;
}
const NonInjectedPreferences = ({
children,
closePreferences,
...props
}: PreferencesProps & Dependencies) => (
<SettingLayout
navigation={<PreferencesNavigation />}
className="Preferences"
contentGaps={false}
closeButtonProps={{ "data-testid": "close-preferences" }}
back={closePreferences}
{...props}
>
{children}
</SettingLayout>
);
export const Preferences = withInjectables<Dependencies, PreferencesProps>(
NonInjectedPreferences,
{
getProps: (di, props) => ({
closePreferences: di.inject(closePreferencesInjectable),
...props,
}),
},
);
@@ -1,21 +0,0 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import proxyPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/proxy/proxy-preferences-route.injectable";
import { LensProxy } from "./proxy";
const proxyPreferencesRouteComponentInjectable = getInjectable({
id: "proxy-preferences-route-component",
instantiate: (di) => ({
route: di.inject(proxyPreferencesRouteInjectable),
Component: LensProxy,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default proxyPreferencesRouteComponentInjectable;
@@ -1,71 +0,0 @@
/**
* 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 { observer } from "mobx-react";
import React from "react";
import type { UserStore } from "../../../common/user-store";
import { Input } from "../input";
import { SubTitle } from "../layout/sub-title";
import { Switch } from "../switch";
import { Preferences } from "./preferences";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
interface Dependencies {
userStore: UserStore;
}
const NonInjectedLensProxy = observer(({ userStore }: Dependencies) => {
const [proxy, setProxy] = React.useState(userStore.httpsProxy || "");
return (
<Preferences data-testid="proxy-preferences-page">
<section id="proxy">
<section>
<h2 data-testid="proxy-header">Proxy</h2>
<SubTitle title="HTTP Proxy" />
<Input
theme="round-black"
placeholder="Type HTTP proxy url (example: http://proxy.acme.org:8080)"
value={proxy}
onChange={v => setProxy(v)}
onBlur={() => userStore.httpsProxy = proxy}
/>
<small className="hint">
Proxy is used only for non-cluster communication.
</small>
</section>
<hr className="small" />
<section className="small">
<SubTitle title="Certificate Trust" />
<Switch
checked={userStore.allowUntrustedCAs}
onChange={() => userStore.allowUntrustedCAs = !userStore.allowUntrustedCAs}
>
Allow untrusted Certificate Authorities
</Switch>
<small className="hint">
This will make Lens to trust ANY certificate authority without any validations.
{" "}
Needed with some corporate proxies that do certificate re-writing.
{" "}
Does not affect cluster communications!
</small>
</section>
</section>
</Preferences>
);
});
export const LensProxy = withInjectables<Dependencies>(
NonInjectedLensProxy,
{
getProps: (di) => ({
userStore: di.inject(userStoreInjectable),
}),
},
);
@@ -1,8 +0,0 @@
.item {
--flex-gap: 0.5rem;
background: var(--inputControlBackground);
min-height: 65px;
border-radius: 3px;
padding: calc(var(--padding) * 1.5);
}
@@ -1,35 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./removable-item.module.scss";
import type { DOMAttributes } from "react";
import React from "react";
import { cssNames } from "../../utils";
import { Icon } from "../icon";
export interface RemovableItemProps extends DOMAttributes<any>{
icon?: string;
onRemove: () => void;
className?: string;
"data-testid"?: string;
}
export function RemovableItem({ icon, onRemove, children, className, "data-testid": testId, ...rest }: RemovableItemProps) {
return (
<div className={cssNames(styles.item, "flex gaps align-center justify-space-between", className)} {...rest}>
{icon && (
<Icon material={icon}/>
)}
{children}
<Icon
material="delete"
onClick={onRemove}
tooltip="Remove"
data-testid={testId}
/>
</div>
);
}
@@ -1,24 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
import type { AppPreferenceRegistration } from "./app-preferences/app-preference-registration";
export const telemetryPreferenceItemInjectionToken = getInjectionToken<AppPreferenceRegistration>({
id: "telemetry-preference-item-injection-token",
});
const telemetryPreferenceItemsInjectable = getInjectable({
id: "telemetry-preference-items",
instantiate: (di) => {
const computedInjectMany = di.inject(computedInjectManyInjectable);
return computedInjectMany(telemetryPreferenceItemInjectionToken);
},
});
export default telemetryPreferenceItemsInjectable;
@@ -1,21 +0,0 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import telemetryPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/telemetry/telemetry-preferences-route.injectable";
import { Telemetry } from "./telemetry";
const telemetryPreferencesRouteComponentInjectable = getInjectable({
id: "telemetry-preferences-route-component",
instantiate: (di) => ({
route: di.inject(telemetryPreferencesRouteInjectable),
Component: Telemetry,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default telemetryPreferencesRouteComponentInjectable;
@@ -1,81 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { observer } from "mobx-react";
import React from "react";
import type { UserStore } from "../../../common/user-store";
import { Checkbox } from "../checkbox";
import { SubTitle } from "../layout/sub-title";
import { ExtensionSettings } from "./extension-settings";
import type { IComputedValue } from "mobx";
import { withInjectables } from "@ogre-tools/injectable-react";
import { Preferences } from "./preferences";
import telemetryPreferenceItemsInjectable from "./telemetry-preference-items.injectable";
import sentryDataSourceNameInjectable from "../../../common/vars/sentry-dsn-url.injectable";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import type { AppPreferenceRegistration } from "./app-preferences/app-preference-registration";
interface Dependencies {
telemetryPreferenceItems: IComputedValue<AppPreferenceRegistration[]>;
sentryDnsUrl: string;
userStore: UserStore;
}
const NonInjectedTelemetry: React.FC<Dependencies> = ({
telemetryPreferenceItems,
sentryDnsUrl,
userStore,
}) => (
<Preferences data-testid="telemetry-preferences-page">
<section id="telemetry">
<h2 data-testid="telemetry-header">Telemetry</h2>
{telemetryPreferenceItems.get().map((item) => (
<ExtensionSettings
key={item.id}
setting={item}
size="small"
data-testid={`telemetry-preference-item-for-${item.id}`}
/>
))}
{sentryDnsUrl ? (
<React.Fragment key="sentry">
<section
id="sentry"
className="small"
data-testid="telemetry-preferences-for-automatic-error-reporting"
>
<SubTitle title="Automatic Error Reporting" />
<Checkbox
label="Allow automatic error reporting"
value={userStore.allowErrorReporting}
onChange={(value) => userStore.allowErrorReporting = value}
/>
<div className="hint">
<span>
Automatic error reports provide vital information about issues
and application crashes. It is highly recommended to keep this
feature enabled to ensure fast turnaround for issues you might
encounter.
</span>
</div>
</section>
<hr className="small" />
</React.Fragment>
) : // we don't need to shows the checkbox at all if Sentry dsn is not a valid url
null}
</section>
</Preferences>
);
export const Telemetry = withInjectables<Dependencies>(
observer(NonInjectedTelemetry),
{
getProps: (di) => ({
telemetryPreferenceItems: di.inject(telemetryPreferenceItemsInjectable),
sentryDnsUrl: di.inject(sentryDataSourceNameInjectable),
userStore: di.inject(userStoreInjectable),
}),
},
);
@@ -1,21 +0,0 @@
/**
* 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 { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
import terminalPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/terminal/terminal-preferences-route.injectable";
import { Terminal } from "./terminal";
const terminalPreferencesRouteComponentInjectable = getInjectable({
id: "terminal-preferences-route-component",
instantiate: (di) => ({
route: di.inject(terminalPreferencesRouteInjectable),
Component: Terminal,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default terminalPreferencesRouteComponentInjectable;
@@ -1,138 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { action } from "mobx";
import { observer } from "mobx-react";
import type { UserStore } from "../../../common/user-store";
import { SubTitle } from "../layout/sub-title";
import { Input } from "../input";
import { Switch } from "../switch";
import { Select, type SelectOption } from "../select";
import type { ThemeStore } from "../../themes/store";
import { Preferences } from "./preferences";
import { withInjectables } from "@ogre-tools/injectable-react";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import themeStoreInjectable from "../../themes/store.injectable";
import defaultShellInjectable from "./default-shell.injectable";
import logger from "../../../common/logger";
interface Dependencies {
userStore: UserStore;
themeStore: ThemeStore;
defaultShell: string;
}
const NonInjectedTerminal = observer((
{
userStore,
themeStore,
defaultShell,
}: Dependencies) => {
const themeOptions = [
{
value: "", // TODO: replace with a sentinal value that isn't string (and serialize it differently)
label: "Match Lens Theme",
},
...Array.from(themeStore.themes, ([themeId, { name }]) => ({
value: themeId,
label: name,
})),
];
// fonts must be declared in `fonts.scss` and at `template.html` (if early-preloading required)
const supportedCustomFonts: SelectOption<string>[] = [
"RobotoMono", "Anonymous Pro", "IBM Plex Mono", "JetBrains Mono", "Red Hat Mono",
"Source Code Pro", "Space Mono", "Ubuntu Mono",
].map(customFont => {
const { fontFamily, fontSize } = userStore.terminalConfig;
return {
label: <span style={{ fontFamily: customFont, fontSize }}>{customFont}</span>,
value: customFont,
isSelected: fontFamily === customFont,
};
});
const onFontFamilyChange = action(({ value: fontFamily }: SelectOption<string>) => {
logger.info(`setting terminal font to ${fontFamily}`);
userStore.terminalConfig.fontFamily = fontFamily; // save to external storage
});
return (
<Preferences data-testid="terminal-preferences-page">
<section>
<h2>Terminal</h2>
<section id="shell">
<SubTitle title="Terminal Shell Path" />
<Input
theme="round-black"
placeholder={defaultShell}
value={userStore.shell ?? ""}
onChange={(value) => userStore.shell = value}
/>
</section>
<section id="terminalSelection">
<SubTitle title="Terminal copy & paste" />
<Switch
checked={userStore.terminalCopyOnSelect}
onChange={() => userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect}
>
Copy on select and paste on right-click
</Switch>
</section>
<section id="terminalTheme">
<SubTitle title="Terminal theme" />
<Select
id="terminal-theme-input"
themeName="lens"
options={themeOptions}
value={userStore.terminalTheme}
onChange={option => userStore.terminalTheme = option?.value ?? ""}
/>
</section>
<section>
<SubTitle title="Font size" />
<Input
theme="round-black"
type="number"
min={10}
max={50}
defaultValue={userStore.terminalConfig.fontSize.toString()}
onChange={(value) => userStore.terminalConfig.fontSize = Number(value)}
/>
</section>
<section>
<SubTitle title="Font family" />
<Select
themeName="lens"
controlShouldRenderValue
value={userStore.terminalConfig.fontFamily}
options={supportedCustomFonts}
onChange={onFontFamilyChange as any}
/>
</section>
</section>
</Preferences>
);
});
export const Terminal = withInjectables<Dependencies>(
NonInjectedTerminal,
{
getProps: (di) => ({
userStore: di.inject(userStoreInjectable),
themeStore: di.inject(themeStoreInjectable),
defaultShell: di.inject(defaultShellInjectable),
}),
},
);
@@ -15,7 +15,6 @@ import { getInjectable } from "@ogre-tools/injectable";
import commandOverlayInjectable from "../command-overlay.injectable";
import createTerminalTabInjectable from "../../dock/terminal/create-terminal-tab.injectable";
import type { DockTabCreate } from "../../dock/dock/store";
import navigateToPreferencesInjectable from "../../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import navigateToHelmChartsInjectable from "../../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import navigateToHelmReleasesInjectable from "../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import navigateToConfigMapsInjectable from "../../../../common/front-end-routing/routes/cluster/config/config-maps/navigate-to-config-maps.injectable";
@@ -40,6 +39,9 @@ import navigateToCustomResourcesInjectable from "../../../../common/front-end-ro
import navigateToEntitySettingsInjectable from "../../../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable";
import getEntitySettingCommandsInjectable from "./get-entity-setting-commands.injectable";
// TODO: Importing from features is not OK. Make commands to comply with Open Closed Principle to allow moving implementation under a feature
import navigateToPreferencesInjectable from "../../../../features/preferences/common/navigate-to-preferences.injectable";
export function isKubernetesClusterActive(context: CommandContext): boolean {
return context.entity?.kind === "KubernetesCluster";
}
@@ -0,0 +1,15 @@
@mixin gutterSize(
$sizeName,
$size
) {
.size-#{$sizeName} {
min-width: $size;
min-height: $size;
}
}
$baseline: 8px;
@include gutterSize('sm', 2 * $baseline);
@include gutterSize('md', 3 * $baseline);
@include gutterSize('xl', 5 * $baseline);
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { cssNames } from "../../utils";
import styles from "./gutter.module.scss";
interface GutterProps {
size?: "sm" | "md" | "xl";
}
const Gutter = ({ size = "md" }: GutterProps) => {
const classNames = cssNames(styles[`size-${size}`]);
return <div className={classNames} />;
};
export default Gutter;
@@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.HorizontalLine {
height: 1px;
border-top: thin solid var(--hrColor);
}
@mixin horizontalLineSize(
$sizeName,
$size
) {
.size-#{$sizeName} {
margin-top: $size;
margin-bottom: $size;
}
}
$baseline: 8px;
@include horizontalLineSize('sm', 2 * $baseline);
@include horizontalLineSize('md', 3 * $baseline);
@include horizontalLineSize('xl', 5 * $baseline);
@@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import styles from "./horizontal-line.module.scss";
import { cssNames } from "../../utils";
interface HorizontalLineProps {
size?: "sm" | "md" | "xl";
}
export const HorizontalLine = ({ size = "xl" }: HorizontalLineProps = { size: "xl" }) => {
const classNames = cssNames(styles.HorizontalLine, styles[`size-${size}`]);
return <div className={classNames} />;
};
@@ -121,14 +121,6 @@
display: flex;
flex-direction: column;
&:not(:first-of-type) {
margin-top: 40px;
&.small {
margin-top: 20px;
}
}
h1, h2 {
color: var(--textColorAccent);
}
@@ -167,15 +159,14 @@
hr {
margin-top: 40px;
margin-bottom: 40px;
height: 1px;
border-top: thin solid var(--hrColor);
&.small {
margin-top: 20px;
}
&:last-child {
display: none;
margin-bottom: 20px;
}
}
@@ -0,0 +1,8 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import toggleMaximizeWindowInjectable from "./toggle-maximize-window.injectable";
import { getGlobalOverrideForFunction } from "../../../../../common/test-utils/get-global-override-for-function";
export default getGlobalOverrideForFunction(toggleMaximizeWindowInjectable);
@@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { WindowAction } from "../../../../common/ipc/window";
import { requestWindowAction } from "../../../ipc";
import { WindowAction } from "../../../../../common/ipc/window";
import { requestWindowAction } from "../../../../ipc";
const toggleMaximizeWindowInjectable = getInjectable({
id: "toggle-maximize-window",
@@ -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 isLinuxInjectable from "../../../../../../common/vars/is-linux.injectable";
import isWindowsInjectable from "../../../../../../common/vars/is-windows.injectable";
import { computed } from "mobx";
import { ContextMenu } from "./context-menu";
import { topBarItemOnLeftSideInjectionToken } from "../top-bar-item-injection-token";
const contextMenuTopBarItemInjectable = getInjectable({
id: "context-menu-top-bar-item",
instantiate: (di) => {
const isWindows = di.inject(isWindowsInjectable);
const isLinux = di.inject(isLinuxInjectable);
return {
id: "context-menu",
isShown: computed(() => isWindows || isLinux),
orderNumber: 10,
Component: ContextMenu,
};
},
injectionToken: topBarItemOnLeftSideInjectionToken,
});
export default contextMenuTopBarItemInjectable;
@@ -0,0 +1,39 @@
/**
* 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 styles from "../../top-bar.module.scss";
import openAppContextMenuInjectable from "./open-app-context-menu/open-app-context-menu.injectable";
interface Dependencies {
openAppContextMenu: () => void;
}
const NonInjectedContextMenu = ({ openAppContextMenu }: Dependencies) => (
<div className={styles.winMenu}>
<div onClick={openAppContextMenu} data-testid="window-menu">
<svg
width="12"
height="12"
viewBox="0 0 12 12"
shapeRendering="crispEdges"
>
<path fill="currentColor" d="M0,8.5h12v1H0V8.5z" />
<path fill="currentColor" d="M0,5.5h12v1H0V5.5z" />
<path fill="currentColor" d="M0,2.5h12v1H0V2.5z" />
</svg>
</div>
</div>
);
export const ContextMenu = withInjectables<Dependencies>(
NonInjectedContextMenu,
{
getProps: (di) => ({
openAppContextMenu: di.inject(openAppContextMenuInjectable),
}),
},
);
@@ -0,0 +1,8 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import openAppContextMenuInjectable from "./open-app-context-menu.injectable";
import { getGlobalOverrideForFunction } from "../../../../../../../common/test-utils/get-global-override-for-function";
export default getGlobalOverrideForFunction(openAppContextMenuInjectable);
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { emitOpenAppMenuAsContextMenu } from "../../../ipc";
import { emitOpenAppMenuAsContextMenu } from "../../../../../../ipc";
const openAppContextMenuInjectable = getInjectable({
id: "open-app-context-menu",
@@ -0,0 +1,8 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import goBackInjectable from "./go-back.injectable";
import { getGlobalOverrideForFunction } from "../../../../../../../common/test-utils/get-global-override-for-function";
export default getGlobalOverrideForFunction(goBackInjectable);
@@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { WindowAction } from "../../../../common/ipc/window";
import { requestWindowAction } from "../../../ipc";
import { WindowAction } from "../../../../../../../common/ipc/window";
import { requestWindowAction } from "../../../../../../ipc";
const goBackInjectable = getInjectable({
id: "go-back",
@@ -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 { computed } from "mobx";
import { topBarItemOnLeftSideInjectionToken } from "../top-bar-item-injection-token";
import { NavigationToBack } from "./navigation-to-back";
const navigationToBackTopBarItemInjectable = getInjectable({
id: "navigation-to-back-top-bar-item",
instantiate: () => ({
id: "navigation-to-back",
isShown: computed(() => true),
orderNumber: 30,
Component: NavigationToBack,
}),
injectionToken: topBarItemOnLeftSideInjectionToken,
});
export default navigationToBackTopBarItemInjectable;
@@ -0,0 +1,39 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import { Icon } from "../../../../icon";
import topBarPrevEnabledInjectable from "./prev-enabled.injectable";
import goBackInjectable from "./go-back/go-back.injectable";
import { observer } from "mobx-react";
interface Dependencies {
prevEnabled: IComputedValue<boolean>;
goBack: () => void;
}
const NonInjectedNavigationToBack = observer(({
prevEnabled,
goBack,
}: Dependencies) => (
<Icon
data-testid="history-back"
material="arrow_back"
onClick={goBack}
disabled={!prevEnabled.get()}
/>
));
export const NavigationToBack = withInjectables<Dependencies>(
NonInjectedNavigationToBack,
{
getProps: (di) => ({
prevEnabled: di.inject(topBarPrevEnabledInjectable),
goBack: di.inject(goBackInjectable),
}),
},
);
@@ -4,7 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import topBarStateInjectable from "./state.injectable";
import topBarStateInjectable from "../../state.injectable";
const topBarPrevEnabledInjectable = getInjectable({
id: "top-bar-prev-enabled",
@@ -0,0 +1,8 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import goForwardInjectable from "./go-forward.injectable";
import { getGlobalOverrideForFunction } from "../../../../../../../common/test-utils/get-global-override-for-function";
export default getGlobalOverrideForFunction(goForwardInjectable);
@@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { WindowAction } from "../../../../common/ipc/window";
import { requestWindowAction } from "../../../ipc";
import { WindowAction } from "../../../../../../../common/ipc/window";
import { requestWindowAction } from "../../../../../../ipc";
const goForwardInjectable = getInjectable({
id: "go-forward",
Loaded 100 of 418 files, more files were not shown because too many files have changed in this diff. Show more