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 -1
View File
@@ -34,7 +34,7 @@ import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import releaseChannelInjectable from "../vars/release-channel.injectable";
import defaultUpdateChannelInjectable from "../application-update/selected-update-channel/default-update-channel.injectable";
import defaultUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/default-update-channel.injectable";
console = new Console(stdout, stderr);
@@ -1,28 +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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
import type { UpdateChannel } from "../update-channels";
import type { SyncBox } from "../../utils/sync-box/sync-box-injection-token";
import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
export type DiscoveredUpdateVersion = SyncBox<{ version: string; updateChannel: UpdateChannel } | null>;
const discoveredUpdateVersionInjectable = getInjectable({
id: "discovered-update-version",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox(
"discovered-update-version",
null,
) as DiscoveredUpdateVersion;
},
injectionToken: syncBoxInjectionToken,
});
export default discoveredUpdateVersionInjectable;
@@ -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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
import type { SyncBox } from "../../utils/sync-box/sync-box-injection-token";
import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
export interface ProgressOfDownload {
percentage: number;
failed?: string;
}
export type ProgressOfUpdateDownload = SyncBox<ProgressOfDownload>;
const progressOfUpdateDownloadInjectable = getInjectable({
id: "progress-of-update-download-state",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox<ProgressOfDownload>("progress-of-update-download", { percentage: 0 });
},
injectionToken: syncBoxInjectionToken,
});
export default progressOfUpdateDownloadInjectable;
@@ -1,11 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token";
export type RestartAndInstallUpdateChannel = MessageChannel<void>;
export const restartAndInstallUpdateChannel: RestartAndInstallUpdateChannel = {
id: "restart-and-install-update-channel",
};
@@ -1,14 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { createInitializableState } from "../../initializable-state/create";
import releaseChannelInjectable from "../../vars/release-channel.injectable";
import { updateChannels } from "../update-channels";
const defaultUpdateChannelInjectable = createInitializableState({
id: "default-update-channel",
init: (di) => updateChannels[di.inject(releaseChannelInjectable).get()],
});
export default defaultUpdateChannelInjectable;
@@ -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 type { IComputedValue } from "mobx";
import { action, computed, observable } from "mobx";
import type { UpdateChannel, ReleaseChannel } from "../update-channels";
import { updateChannels } from "../update-channels";
import defaultUpdateChannelInjectable from "./default-update-channel.injectable";
export interface SelectedUpdateChannel {
value: IComputedValue<UpdateChannel>;
setValue: (channelId?: ReleaseChannel) => void;
}
const selectedUpdateChannelInjectable = getInjectable({
id: "selected-update-channel",
instantiate: (di): SelectedUpdateChannel => {
const defaultUpdateChannel = di.inject(defaultUpdateChannelInjectable);
const state = observable.box<UpdateChannel>(undefined, { deep: false });
return {
value: computed(() => state.get() ?? defaultUpdateChannel.get()),
setValue: action((channelId) => {
const targetUpdateChannel =
channelId && updateChannels[channelId]
? updateChannels[channelId]
: defaultUpdateChannel.get();
state.set(targetUpdateChannel);
}),
};
},
});
export default selectedUpdateChannelInjectable;
@@ -1,37 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type ReleaseChannel = "alpha" | "beta" | "latest";
const latestChannel: UpdateChannel = {
id: "latest",
label: "Stable",
moreStableUpdateChannel: null,
};
const betaChannel: UpdateChannel = {
id: "beta",
label: "Beta",
moreStableUpdateChannel: latestChannel,
};
const alphaChannel: UpdateChannel = {
id: "alpha",
label: "Alpha",
moreStableUpdateChannel: betaChannel,
};
export const updateChannels = {
latest: latestChannel,
beta: betaChannel,
alpha: alphaChannel,
};
export interface UpdateChannel {
readonly id: ReleaseChannel;
readonly label: string;
readonly moreStableUpdateChannel: UpdateChannel | null;
}
@@ -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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
const updateDownloadedDateTimeInjectable = getInjectable({
id: "update-downloaded-date-time",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox<string | null>(
"update-downloaded-date-time",
null,
);
},
injectionToken: syncBoxInjectionToken,
});
export default updateDownloadedDateTimeInjectable;
@@ -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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
import type { SyncBox } from "../../utils/sync-box/sync-box-injection-token";
import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
export type UpdateIsBeingDownloaded = SyncBox<boolean>;
const updateIsBeingDownloadedInjectable = getInjectable({
id: "update-is-being-downloaded",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox("update-is-being-downloaded", false);
},
injectionToken: syncBoxInjectionToken,
});
export default updateIsBeingDownloadedInjectable;
@@ -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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
import type { SyncBox } from "../../utils/sync-box/sync-box-injection-token";
import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
export type UpdatesAreBeingDiscovered = SyncBox<boolean>;
const updatesAreBeingDiscoveredInjectable = getInjectable({
id: "updates-are-being-discovered",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox("updates-are-being-discovered", false);
},
injectionToken: syncBoxInjectionToken,
});
export default updatesAreBeingDiscoveredInjectable;
@@ -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 { generalCatalogEntityInjectionToken } from "../general-catalog-entity-injection-token";
import { GeneralEntity } from "../../index";
import { buildURL } from "../../../utils/buildUrl";
import appPreferencesRouteInjectable from "../../../front-end-routing/routes/preferences/app/app-preferences-route.injectable";
const preferencesCatalogEntityInjectable = getInjectable({
id: "general-catalog-entity-for-preferences",
instantiate: (di) => {
const route = di.inject(appPreferencesRouteInjectable);
const url = buildURL(route.path);
return new GeneralEntity({
metadata: {
uid: "preferences-entity",
name: "Preferences",
source: "app",
labels: {},
},
spec: {
path: url,
icon: {
material: "settings",
background: "#3d90ce",
},
},
status: {
phase: "active",
},
});
},
injectionToken: generalCatalogEntityInjectionToken,
});
export default preferencesCatalogEntityInjectable;
@@ -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 { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const appPreferencesRouteInjectable = getInjectable({
id: "app-preferences-route",
instantiate: () => ({
path: "/preferences/app",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default appPreferencesRouteInjectable;
@@ -1,20 +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 appPreferencesRouteInjectable from "./app-preferences-route.injectable";
import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token";
const navigateToAppPreferencesInjectable = getInjectable({
id: "navigate-to-app-preferences",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(appPreferencesRouteInjectable);
return () => navigateToRoute(route);
},
});
export default navigateToAppPreferencesInjectable;
@@ -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 { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const editorPreferencesRouteInjectable = getInjectable({
id: "editor-preferences-route",
instantiate: () => ({
path: "/preferences/editor",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default editorPreferencesRouteInjectable;
@@ -1,20 +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 editorPreferencesRouteInjectable from "./editor-preferences-route.injectable";
import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token";
const navigateToEditorPreferencesInjectable = getInjectable({
id: "navigate-to-editor-preferences",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(editorPreferencesRouteInjectable);
return () => navigateToRoute(route);
},
});
export default navigateToEditorPreferencesInjectable;
@@ -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 { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
import type { Route } from "../../../front-end-route-injection-token";
interface ExtensionPreferenceRouteParams {
extensionId: string;
tabId?: string;
}
const extensionPreferencesRouteInjectable = getInjectable({
id: "extension-preferences-route",
instantiate: (): Route<ExtensionPreferenceRouteParams> => ({
path: "/preferences/extension/:extensionId/:tabId?",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default extensionPreferencesRouteInjectable;
@@ -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 extensionPreferencesRouteInjectable from "./extension-preferences-route.injectable";
import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token";
const navigateToExtensionPreferencesInjectable = getInjectable({
id: "navigate-to-extension-preferences",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(extensionPreferencesRouteInjectable);
return (extensionId: string, tabId?: string) => navigateToRoute(route, {
parameters: {
extensionId,
tabId,
},
withoutAffectingBackButton: true,
});
},
});
export default navigateToExtensionPreferencesInjectable;
@@ -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 { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const kubernetesPreferencesRouteInjectable = getInjectable({
id: "kubernetes-preferences-route",
instantiate: () => ({
path: "/preferences/kubernetes",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default kubernetesPreferencesRouteInjectable;
@@ -1,20 +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 kubernetesPreferencesRouteInjectable from "./kubernetes-preferences-route.injectable";
import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token";
const navigateToKubernetesPreferencesInjectable = getInjectable({
id: "navigate-to-kubernetes-preferences",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(kubernetesPreferencesRouteInjectable);
return () => navigateToRoute(route);
},
});
export default navigateToKubernetesPreferencesInjectable;
@@ -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";
import navigateToAppPreferencesInjectable from "./app/navigate-to-app-preferences.injectable";
const navigateToPreferencesInjectable = getInjectable({
id: "navigate-to-preferences",
instantiate: (di) => di.inject(navigateToAppPreferencesInjectable),
});
export default navigateToPreferencesInjectable;
@@ -1,20 +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 proxyPreferencesRouteInjectable from "./proxy-preferences-route.injectable";
import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token";
const navigateToProxyPreferencesInjectable = getInjectable({
id: "navigate-to-proxy-preferences",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(proxyPreferencesRouteInjectable);
return () => navigateToRoute(route);
},
});
export default navigateToProxyPreferencesInjectable;
@@ -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 { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const proxyPreferencesRouteInjectable = getInjectable({
id: "proxy-preferences-route",
instantiate: () => ({
path: "/preferences/proxy",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default proxyPreferencesRouteInjectable;
@@ -1,20 +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 telemetryPreferencesRouteInjectable from "./telemetry-preferences-route.injectable";
import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token";
const navigateToTelemetryPreferencesInjectable = getInjectable({
id: "navigate-to-telemetry-preferences",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(telemetryPreferencesRouteInjectable);
return () => navigateToRoute(route);
},
});
export default navigateToTelemetryPreferencesInjectable;
@@ -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 { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const telemetryPreferencesRouteInjectable = getInjectable({
id: "telemetry-preferences-route",
instantiate: () => ({
path: "/preferences/telemetry",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default telemetryPreferencesRouteInjectable;
@@ -1,20 +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 terminalPreferencesRouteInjectable from "./terminal-preferences-route.injectable";
import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token";
const navigateToTerminalPreferencesInjectable = getInjectable({
id: "navigate-to-terminal-preferences",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(terminalPreferencesRouteInjectable);
return () => navigateToRoute(route);
},
});
export default navigateToTerminalPreferencesInjectable;
@@ -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 { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const terminalPreferencesRouteInjectable = getInjectable({
id: "terminal-preferences-route",
instantiate: () => ({
path: "/preferences/terminal",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default terminalPreferencesRouteInjectable;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverrideForFunction } from "./test-utils/get-global-override-for-function";
import logErrorInjectable from "./log-error.injectable";
// Note: this should remain as it is, and throw if called. Logging error is something
// that cannot happen without a unit test explicitly causing it. It cannot be allowed
// to happen without author of unit test knowing it.
export default getGlobalOverrideForFunction(logErrorInjectable);
@@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { getGlobalOverride } from "./get-global-override";
import { camelCase } from "lodash/fp";
export const getGlobalOverrideForFunction = (
injectable: Injectable<Function, any, any>,
) =>
getGlobalOverride(injectable, () => (...args: any[]) => {
console.warn(
`Tried to invoke a function "${injectable.id}" without override. The args were:`,
args,
);
throw new Error(
`Tried to invoke a function "${
injectable.id
}" without override. Add eg. "di.override(${camelCase(
injectable.id,
)}Mock)" to the unit test interested in this.`,
);
});
+1 -1
View File
@@ -16,7 +16,7 @@ export const advanceFakeTime = (milliseconds: number) => {
});
};
export const useFakeTime = (dateTime: string) => {
export const useFakeTime = (dateTime = "2015-10-21T07:28:00Z") => {
usingFakeTime = true;
jest.useFakeTimers();
@@ -4,7 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { UserStore } from "./user-store";
import selectedUpdateChannelInjectable from "../application-update/selected-update-channel/selected-update-channel.injectable";
import selectedUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/selected-update-channel.injectable";
const userStoreInjectable = getInjectable({
id: "user-store",
+4 -2
View File
@@ -11,8 +11,10 @@ import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils";
import { DESCRIPTORS } from "./preferences-helpers";
import type { UserPreferencesModel, StoreType } from "./preferences-helpers";
import logger from "../../main/logger";
import type { SelectedUpdateChannel } from "../application-update/selected-update-channel/selected-update-channel.injectable";
import type { ReleaseChannel } from "../application-update/update-channels";
// TODO: Remove coupling with Feature
import type { SelectedUpdateChannel } from "../../features/application-update/common/selected-update-channel/selected-update-channel.injectable";
import type { ReleaseChannel } from "../../features/application-update/common/update-channels";
export interface UserStoreModel {
lastSeenAppVersion: string;
@@ -0,0 +1,53 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { addSeparator } from "./add-separator";
describe("add-separator", () => {
it("given multiple items, adds separators", () => {
const items = ["first", "second", "third"];
const actual = addSeparator((left, right) => `separator-between-${left}-and-${right}`, items);
expect(actual).toEqual([
"first",
"separator-between-first-and-second",
"second",
"separator-between-second-and-third",
"third",
]);
});
it("given multiple items including falsy ones, adds separators", () => {
const items = [false, undefined, null, NaN];
const actual = addSeparator((left, right) => `separator-between-${left}-and-${right}`, items);
expect(actual).toEqual([
false,
"separator-between-false-and-undefined",
undefined,
"separator-between-undefined-and-null",
null,
"separator-between-null-and-NaN",
NaN,
]);
});
it("given no items, does not add separator", () => {
const items: any[] = [];
const actual = addSeparator(() => "separator", items);
expect(actual).toEqual([]);
});
it("given one item, does not add separator", () => {
const items = ["first"];
const actual = addSeparator(() => "separator", items);
expect(actual).toEqual(["first"]);
});
});
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type GetSeparator<Item, Separator> = (left: Item, right: Item) => Separator;
export const addSeparator = <Item, Separator>(
getSeparator: GetSeparator<Item, Separator>,
items: Item[],
) => items.flatMap(toSeparatedTupleUsing(getSeparator));
const toSeparatedTupleUsing =
<Item, Separator>(getSeparator: GetSeparator<Item, Separator>) =>
(leftItem: Item, index: number, arr: Item[]) => {
const itemIsLast = arr.length === index + 1;
if (itemIsLast) {
return [leftItem];
}
const rightItem = arr[index + 1];
const separator = getSeparator(leftItem, rightItem);
return [leftItem, separator];
};
@@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// See: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions
export interface Discriminable<T extends string> { readonly kind: T }
// Note: this will fail at transpilation time, if all kinds are not instructed in switch/case.
// See: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking
export const checkThatAllDiscriminablesAreExhausted = <T extends never>(value: T) => {
const _exhaustiveCheck: never = value;
return new Error(
`Tried to exhaust discriminables, but no instructions were found for ${(_exhaustiveCheck as any).kind}`,
);
};
@@ -0,0 +1,7 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export interface Labelable {
readonly label: string;
}
@@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { sortBy } from "lodash/fp";
export interface Orderable {
readonly orderNumber: number;
}
export type MaybeOrderable = Orderable | object;
export const orderByOrderNumber = <T extends MaybeOrderable>(maybeOrderables: T[]) =>
sortBy(
(orderable) =>
"orderNumber" in orderable
? orderable.orderNumber
: Number.MAX_SAFE_INTEGER,
maybeOrderables,
);
@@ -0,0 +1,29 @@
/**
* 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 { isBoolean } from "../../type-narrowing";
export interface Showable {
readonly isShown: IComputedValue<boolean> | boolean;
}
export type MaybeShowable = Showable | object;
export const isShown = (showable: MaybeShowable) => {
if (!("isShown" in showable)) {
return true;
}
if (showable.isShown === undefined) {
return true;
}
if (isBoolean(showable.isShown)) {
return showable.isShown;
}
return showable.isShown.get();
};
@@ -0,0 +1,61 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite/get-composite";
import { compositeHasDescendant } from "./composite-has-descendant";
import { getCompositeFor } from "../get-composite/get-composite";
describe("composite-has-descendant, given composite with children and grand children", () => {
let composite: Composite<{ id: string; parentId?: string }>;
beforeEach(() => {
const items = [
{ id: "some-root-id", parentId: undefined },
{ id: "some-child-item", parentId: "some-root-id" },
{
id: "some-grand-child-item",
parentId: "some-child-item",
},
];
const getComposite = getCompositeFor<{
id: string;
parentId?: string;
}>({
rootId: "some-root-id",
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
composite = getComposite(items);
});
it("has a child as descendant", () => {
const actual = compositeHasDescendant<typeof composite["value"]>(
(referenceComposite) => referenceComposite.value.id === "some-child-item",
)(composite);
expect(actual).toBe(true);
});
it("has a grand child as descendant", () => {
const actual = compositeHasDescendant<typeof composite["value"]>(
(referenceComposite) =>
referenceComposite.value.id === "some-grand-child-item",
)(composite);
expect(actual).toBe(true);
});
it("does not have an unrelated descendant", () => {
const actual = compositeHasDescendant<typeof composite["value"]>(
(referenceComposite) =>
referenceComposite.value.id === "some-unknown-item",
)(composite);
expect(actual).toBe(false);
});
});
@@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite/get-composite";
const compositeHasDescendant = <T>(
predicate: (referenceComposite: Composite<T>) => boolean,
) => {
const _compositeHasDescendant = (composite: Composite<T>): boolean =>
predicate(composite) ||
!!composite.children.find((childComposite) =>
_compositeHasDescendant(childComposite),
);
return _compositeHasDescendant;
};
export { compositeHasDescendant };
@@ -0,0 +1,91 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite/get-composite";
import { findComposite } from "./find-composite";
import { getCompositeFor } from "../get-composite/get-composite";
describe("find-composite", () => {
let composite: Composite<{ id: string; parentId?: string }>;
beforeEach(() => {
const getComposite = getCompositeFor<{
id: string;
parentId?: string;
}>({
rootId: "some-root-id",
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
composite = getComposite([
{ id: "some-root-id" },
{ id: "some-child-id", parentId: "some-root-id" },
{ id: "some-grandchild-id", parentId: "some-child-id" },
{ id: "some-other-grandchild-id", parentId: "some-child-id" },
]);
});
it("when finding root using path, does so", () => {
const actual = findComposite("some-root-id")(composite);
expect(actual.id).toBe("some-root-id");
});
it("when finding child using path, does so", () => {
const actual = findComposite("some-root-id", "some-child-id")(composite);
expect(actual.id).toBe("some-child-id");
});
it("when finding grandchild using path, does so", () => {
const actual = findComposite(
"some-root-id",
"some-child-id",
"some-grandchild-id",
)(composite);
expect(actual.id).toBe("some-grandchild-id");
});
it("when finding with non existing leaf-level path, throws", () => {
expect(() => {
findComposite(
"some-root-id",
"some-child-id",
"some-non-existing-grandchild-id",
)(composite);
}).toThrow(`Tried to find 'some-root-id -> some-child-id -> some-non-existing-grandchild-id' from a composite, but found nothing.
Node 'some-root-id -> some-child-id' had only following children:
some-grandchild-id
some-other-grandchild-id`);
});
it("when finding with non-existing mid-level path, throws", () => {
expect(() => {
findComposite(
"some-root-id",
"some-non-existing-child-id",
"some-non-existing-grandchild-id",
)(composite);
}).toThrow(`Tried to find 'some-root-id -> some-non-existing-child-id -> some-non-existing-grandchild-id' from a composite, but found nothing.
Node 'some-root-id' had only following children:
some-child-id`);
});
it("when finding with non-existing root-level path, throws", () => {
expect(() => {
findComposite(
"some-non-existing-root-id",
"some-non-existing-child-id",
"some-non-existing-grandchild-id",
)(composite);
}).toThrow(`Tried to find 'some-non-existing-root-id -> some-non-existing-child-id -> some-non-existing-grandchild-id' from a composite, but found nothing.
Node 'some-root-id' had only following children:
some-child-id`);
});
});
@@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite/get-composite";
const _findComposite = <T>(currentLeftIds: string[], currentId: string, currentRightIds: string[], composite: Composite<T>): Composite<T> => {
const [nextId, ...nextRightIds] = currentRightIds;
const nextLeftIds = [...currentLeftIds, currentId];
if (currentRightIds.length === 0 && composite.id === currentId) {
return composite;
}
const foundChildComposite = composite.children.find((child) => child.id === nextId);
if (foundChildComposite) {
return _findComposite(nextLeftIds, nextId, nextRightIds, foundChildComposite);
}
const fullPathString = [...currentLeftIds, currentId, ...currentRightIds].join(" -> ");
throw new Error(`Tried to find '${fullPathString}' from a composite, but found nothing.
Node '${[...currentLeftIds, composite.id].join(" -> ")}' had only following children:
${composite.children.map((child) => child.id).join("\n")}`);
};
export const findComposite =
(...path: string[]) =>
<T>(composite: Composite<T>): Composite<T> => {
const [currentId, ...rightIds] = path;
const leftIds: string[] = [];
return _findComposite(leftIds, currentId, rightIds, composite);
};
@@ -0,0 +1,53 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getCompositeNormalization } from "./get-composite-normalization";
import { getCompositeFor } from "../get-composite/get-composite";
describe("get-composite-normalization", () => {
it("given a composite, flattens it to paths and composites", () => {
const someRootItem = {
id: "some-root-id",
parentId: undefined,
};
const someItem = {
id: "some-id",
parentId: "some-root-id",
};
const someNestedItem = {
id: "some-child-id",
parentId: "some-id",
};
const items = [someRootItem, someItem, someNestedItem];
const getComposite = getCompositeFor<{
id: string;
parentId?: string;
orderNumber?: number;
}>({
rootId: "some-root-id",
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
const composite = getComposite(items);
const actual = getCompositeNormalization(composite);
expect(actual).toEqual([
[["some-root-id"], expect.objectContaining({ value: someRootItem })],
[["some-root-id", "some-id"], expect.objectContaining({ value: someItem })],
[
["some-root-id", "some-id", "some-child-id"],
expect.objectContaining({ value: someNestedItem }),
],
]);
});
});
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite/get-composite";
export const getCompositeNormalization = <T>(composite: Composite<T>) => {
const _normalizeComposite = <T>(
composite: Composite<T>,
previousPath: string[] = [],
): (readonly [path: string[], composite: Composite<T>])[] => {
const currentPath = [...previousPath, composite.id];
const pathAndCompositeTuple = [currentPath, composite] as const;
return [
pathAndCompositeTuple,
...composite.children.flatMap((child) =>
_normalizeComposite(child, currentPath),
),
];
};
return _normalizeComposite(composite);
};
@@ -0,0 +1,71 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getCompositePaths } from "./get-composite-paths";
import { sortBy } from "lodash/fp";
import { getCompositeFor } from "../get-composite/get-composite";
describe("get-composite-paths", () => {
it("given composite with transformed children, returns paths of transformed children in hierarchical order", () => {
const someRootItem = {
id: "some-root-id",
};
const someChildItem1 = {
id: "some-child-id-1",
parentId: "some-root-id",
orderNumber: 1,
};
const someChildItem2 = {
id: "some-child-id-2",
parentId: "some-root-id",
orderNumber: 2,
};
const someGrandchildItem1 = {
id: "some-grandchild-id-1",
parentId: "some-child-id-1",
orderNumber: 1,
};
const someGrandchildItem2 = {
id: "some-grandchild-id-2",
parentId: "some-child-id-1",
orderNumber: 2,
};
const items = [
someRootItem,
// Note: not in order yet.
someChildItem2,
someChildItem1,
someGrandchildItem2,
someGrandchildItem1,
];
const getComposite = getCompositeFor<{
id: string;
parentId?: string;
orderNumber?: number;
}>({
rootId: "some-root-id",
getId: (x) => x.id,
getParentId: (x) => x.parentId,
transformChildren: children => sortBy(child => child.orderNumber, children),
});
const composite = getComposite(items);
const actual = getCompositePaths(composite);
expect(actual).toEqual([
["some-root-id"],
["some-root-id", "some-child-id-1"],
["some-root-id", "some-child-id-1", "some-grandchild-id-1"],
["some-root-id", "some-child-id-1", "some-grandchild-id-2"],
["some-root-id", "some-child-id-2"],
]);
});
});
@@ -0,0 +1,12 @@
/**
* 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 { map } from "lodash/fp";
import type { Composite } from "../get-composite/get-composite";
import { getCompositeNormalization } from "../get-composite-normalization/get-composite-normalization";
export const getCompositePaths = (
composite: Composite<unknown>,
): string[][] => pipeline(composite, getCompositeNormalization, map(([path]) => path));
@@ -0,0 +1,363 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "./get-composite";
import { getCompositePaths } from "../get-composite-paths/get-composite-paths";
import { sortBy } from "lodash/fp";
import { getCompositeFor } from "./get-composite";
interface SomeItem {
id: string;
parentId?: string;
orderNumber?: number;
}
describe("get-composite", () => {
it("given items and an explicit root id, creates a composite", () => {
const someRootItem = {
id: "some-root-id",
someProperty: "some-root-content",
};
const someIrrelevantRootItem = {
id: "some-irrelevant-root-id",
someProperty: "some-other-root-content",
};
const someItem = {
id: "some-id",
parentId: "some-root-id",
someProperty: "some-content",
};
const someNestedItem = {
id: "some-nested-id",
parentId: "some-id",
someProperty: "some-nested-content",
};
const items = [someRootItem, someIrrelevantRootItem, someItem, someNestedItem];
const getComposite = getCompositeFor<SomeItem>({
rootId: "some-root-id",
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
const composite = getComposite(items);
expect(composite).toEqual({
id: "some-root-id",
value: someRootItem,
children: [
{
id: "some-id",
parentId: "some-root-id",
value: someItem,
children: [
{
id: "some-nested-id",
parentId: "some-id",
value: someNestedItem,
children: [],
},
],
},
],
});
});
it("given items and implicit root, creates a composite", () => {
const someRootItem = {
id: "some-root-id",
someProperty: "some-root-content",
// Notice: no "parentId" makes this the implicit root.
parentId: undefined,
};
const someItem = {
id: "some-id",
parentId: "some-root-id",
someProperty: "some-content",
};
const someNestedItem = {
id: "some-nested-id",
parentId: "some-id",
someProperty: "some-nested-content",
};
const items = [someRootItem, someItem, someNestedItem];
const getComposite = getCompositeFor<SomeItem>({
// Notice: no root id
// rootId: "some-root-id",
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
const composite = getComposite(items);
expect(composite).toEqual({
id: "some-root-id",
value: someRootItem,
children: [
{
id: "some-id",
parentId: "some-root-id",
value: someItem,
children: [
{
id: "some-nested-id",
parentId: "some-id",
value: someNestedItem,
children: [],
},
],
},
],
});
});
it("given items and an unspecified root id and multiple items without parent as root, throws", () => {
const someRootItem = {
id: "some-root-id",
// Notice: no "parentId" makes this a root.
parentId: undefined,
};
const someOtherRootItem = {
id: "some-other-root-id",
// Notice: no "parentId" makes also this a root.
parentId: undefined,
};
const items = [someRootItem, someOtherRootItem];
const getComposite = getCompositeFor<SomeItem>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
expect(() => {
getComposite(items);
}).toThrow(
'Tried to get a composite, but multiple roots where encountered: "some-root-id", "some-other-root-id"',
);
});
it("given non-unique ids, throws", () => {
const someItem = {
id: "some-id",
parentId: "irrelevant",
};
const someOtherItem = {
id: "some-id",
parentId: "irrelevant",
};
const items = [someItem, someOtherItem];
const getComposite = getCompositeFor<SomeItem>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
expect(() => {
getComposite(items);
}).toThrow(
'Tried to get a composite but encountered non-unique ids: "some-id"',
);
});
it("given items with missing parent ids, when creating composite without handling for unknown parents, throws", () => {
const someItem = {
id: "some-id",
parentId: undefined,
};
const someItemWithMissingParentId = {
id: "some-other-id",
parentId: "some-missing-id",
};
const items = [someItem, someItemWithMissingParentId];
const getComposite = getCompositeFor<SomeItem>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
expect(() => {
getComposite(items);
}).toThrow(
`Tried to get a composite but encountered missing parent ids: "some-missing-id".
Available parent ids are:
"some-id",
"some-other-id"`,
);
});
describe("given items with missing parents, when creating composite with handling for missing parents", () => {
let composite: Composite<any>;
let handleMissingParentIdMock: jest.Mock;
beforeEach(() => {
const someItem = {
id: "some-root-id",
};
const someItemWithMissingParentId = {
id: "some-orphan-id",
// Note: the item corresponding to this id does not exist,
// making this item have a "missing parent".
parentId: "some-missing-id",
};
const items = [someItem, someItemWithMissingParentId];
handleMissingParentIdMock = jest.fn();
const getComposite = getCompositeFor<SomeItem>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
handleMissingParentIds: handleMissingParentIdMock,
});
composite = getComposite(items);
});
it("creates composite without the orphan item, and without throwing", () => {
const paths = getCompositePaths(composite);
expect(paths).toEqual([["some-root-id"]]);
});
it("handles the missing parent ids", () => {
expect(handleMissingParentIdMock).toHaveBeenCalledWith({
missingParentIds: ["some-missing-id"],
availableParentIds: ["some-root-id", "some-orphan-id"],
});
});
});
it("given items with same id and parent id, throws", () => {
const someItem = {
id: "some-id",
parentId: "some-id",
};
const someRoot = {
id: "root",
parentId: undefined,
};
const items = [someItem, someRoot];
const getComposite = getCompositeFor<SomeItem>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
expect(() => {
getComposite(items);
}).toThrow(
'Tried to get a composite, but found items with self as parent: "some-id"',
);
});
it("given undefined ids, throws", () => {
const root = {
parentId: undefined,
id: "some-root",
};
const someItem = {
parentId: "some-root",
id: undefined,
};
const someOtherItem = {
parentId: "some-root",
id: undefined,
};
const items = [root, someItem, someOtherItem];
const getComposite = getCompositeFor<any>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
expect(() => {
getComposite(items);
}).toThrow("Tried to get a composite but encountered 2 undefined ids");
});
it("given transformed children, creates a composite with transformed children", () => {
const someRootItem = {
id: "some-root-id",
orderNumber: 1,
};
const someItem1 = {
id: "some-id-1",
parentId: "some-root-id",
orderNumber: 1,
};
const someItem2 = {
id: "some-id-2",
parentId: "some-root-id",
orderNumber: 2,
};
const someChildItem1 = {
id: "some-child-id-1",
parentId: "some-id-1",
orderNumber: 1,
};
const someChildItem2 = {
id: "some-child-id-2",
parentId: "some-id-1",
orderNumber: 2,
};
const items = [
someRootItem,
// Note: not in order yet.
someItem2,
someItem1,
someChildItem2,
someChildItem1,
];
const getComposite = getCompositeFor<SomeItem>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
transformChildren: (things) =>
sortBy((thing) => thing.orderNumber, things),
});
const composite = getComposite(items);
const orderedPaths = getCompositePaths(composite);
expect(orderedPaths).toEqual([
["some-root-id"],
["some-root-id", "some-id-1"],
["some-root-id", "some-id-1", "some-child-id-1"],
["some-root-id", "some-id-1", "some-child-id-2"],
["some-root-id", "some-id-2"],
]);
});
});
@@ -0,0 +1,147 @@
/**
* 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 {
countBy,
filter,
toPairs,
nth,
map,
uniq,
without,
compact,
identity,
} from "lodash/fp";
export interface Composite<T> {
id: string;
parentId: string | undefined;
value: T;
children: Composite<T>[];
}
interface Configuration<T> {
rootId?: string;
getId: (thing: T) => string;
getParentId: (thing: T) => string | undefined;
transformChildren?: (things: T[]) => T[];
handleMissingParentIds?: (parentIdsForHandling: ParentIdsForHandling) => void;
}
export const getCompositeFor = <T>({
rootId = undefined,
getId,
getParentId,
transformChildren = identity,
handleMissingParentIds = throwMissingParentIds,
}: Configuration<T>) => (source: T[]) => {
const undefinedIds = pipeline(
source,
filter((x) => getId(x) === undefined),
);
if (undefinedIds.length) {
throw new Error(
`Tried to get a composite but encountered ${undefinedIds.length} undefined ids`,
);
}
const selfReferencingIds = pipeline(
source,
filter((x) => getId(x) === getParentId(x)),
map(getId),
);
if (selfReferencingIds.length) {
throw new Error(
`Tried to get a composite, but found items with self as parent: "${selfReferencingIds.join(
'", ',
)}"`,
);
}
const duplicateIds = pipeline(
source,
countBy(getId),
toPairs,
filter(([, count]) => count > 1),
map(nth(0)),
);
if (duplicateIds.length) {
throw new Error(
`Tried to get a composite but encountered non-unique ids: "${duplicateIds
.map((x) => String(x))
.join('", "')}"`,
);
}
const allIds = pipeline(source, map(getId));
const allParentIds = pipeline(source, map(getParentId), uniq, compact);
const missingParentIds = without(allIds, allParentIds);
if (missingParentIds.length) {
handleMissingParentIds({ missingParentIds, availableParentIds: allIds });
}
const toComposite = (thing: T): Composite<T> => {
const thingId = getId(thing);
return {
id: thingId,
parentId: getParentId(thing),
value: thing,
children: pipeline(
source,
filter((childThing) => {
const parentId = getParentId(childThing);
return parentId === thingId;
}),
transformChildren,
map(toComposite),
),
};
};
const isRootId = rootId
? (thing: T) => getId(thing) === rootId
: (thing: T) => getParentId(thing) === undefined;
const roots = source.filter(isRootId);
if (roots.length > 1) {
throw new Error(
`Tried to get a composite, but multiple roots where encountered: "${roots
.map(getId)
.join('", "')}"`,
);
}
return toComposite(roots[0]);
};
interface ParentIdsForHandling {
missingParentIds: string[];
availableParentIds: string[];
}
const throwMissingParentIds = ({
missingParentIds,
availableParentIds,
}: ParentIdsForHandling) => {
throw new Error(
`Tried to get a composite but encountered missing parent ids: "${missingParentIds.join(
'", "',
)}".\n\nAvailable parent ids are:\n"${availableParentIds.join('",\n"')}"`,
);
};
+16
View File
@@ -0,0 +1,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export interface ParentOfChildComposite<Id extends string = string> {
id: Id;
}
export interface ChildOfParentComposite<ParentId extends string = string> {
parentId: ParentId;
}
export type RootComposite<Id extends string = string> =
& { parentId: undefined }
& ParentOfChildComposite<Id>;
@@ -0,0 +1,34 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { findExactlyOne } from "./find-exactly-one";
describe("find-exactly-one", () => {
it("when predicate matches to single item, returns the item", () => {
const actual = findExactlyOne((item) => item === "some-item")([
"some-item",
"some-other-item",
]);
expect(actual).toBe("some-item");
});
it("when predicate matches to many items, throws", () => {
expect(() => {
findExactlyOne((item) => item === "some-item")([
"some-item",
"some-item",
]);
}).toThrow("Tried to find exactly one, but found many");
});
it("when predicate does not match, throws", () => {
expect(() => {
findExactlyOne((item) => item === "some-item")([
"some-other-item",
]);
}).toThrow("Tried to find exactly one, but didn't find any");
});
});
@@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const findExactlyOne = <T>(predicate: (item: T) => boolean) => (collection: T[]): T => {
const itemsFound = collection.filter(predicate);
if (!itemsFound.length) {
throw new Error(
"Tried to find exactly one, but didn't find any",
);
}
if (itemsFound.length > 1) {
throw new Error(
"Tried to find exactly one, but found many",
);
}
return itemsFound[0];
};
+9
View File
@@ -35,6 +35,15 @@ export function hasTypedProperty<S extends object, K extends PropertyKey, V>(val
return hasOwnProperty(val, key) && isValid(val[key]);
}
/**
* Narrows `val` to include the property `key` with type string
* @param val the value that we are trying to type narrow
* @param key The key to test if it is present on the object (must be a literal for tsc to do any meaningful typing)
*/
export function hasStringProperty<S extends object, K extends PropertyKey>(val: S, key: K): val is (S & { [key in K]: string }) {
return hasOwnProperty(val, key) && isString(val[key]);
}
/**
* Narrows `val` to include the property `key` with type `V | undefined` or doesn't contain it
* @param val the value that we are trying to type narrow
@@ -4,28 +4,25 @@
*/
import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
import loggerInjectable from "../../logger.injectable";
import type { Logger } from "../../logger";
import withErrorLoggingInjectable from "./with-error-logging.injectable";
import { pipeline } from "@ogre-tools/fp";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { getPromiseStatus } from "../../test-utils/get-promise-status";
import logErrorInjectable from "../../log-error.injectable";
describe("with-error-logging", () => {
describe("given decorated sync function", () => {
let loggerStub: Logger;
let toBeDecorated: jest.Mock<number | undefined, [string, string]>;
let decorated: (a: string, b: string) => number | undefined;
let logErrorMock: jest.Mock;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
loggerStub = {
error: jest.fn(),
} as unknown as Logger;
logErrorMock = jest.fn();
di.override(loggerInjectable, () => loggerStub);
di.override(logErrorInjectable, () => logErrorMock);
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
@@ -52,7 +49,7 @@ describe("with-error-logging", () => {
});
it("does not log error", () => {
expect(loggerStub.error).not.toHaveBeenCalled();
expect(logErrorMock).not.toHaveBeenCalled();
});
it("returns the value", () => {
@@ -75,7 +72,7 @@ describe("with-error-logging", () => {
});
it("does not log error", () => {
expect(loggerStub.error).not.toHaveBeenCalled();
expect(logErrorMock).not.toHaveBeenCalled();
});
it("returns nothing", () => {
@@ -104,7 +101,7 @@ describe("with-error-logging", () => {
});
it("logs the error", () => {
expect(loggerStub.error).toHaveBeenCalledWith("some-error-message-for-some-error", error);
expect(logErrorMock).toHaveBeenCalledWith("some-error-message-for-some-error", error);
});
it("throws", () => {
@@ -114,18 +111,16 @@ describe("with-error-logging", () => {
});
describe("given decorated async function", () => {
let loggerStub: Logger;
let decorated: (a: string, b: string) => Promise<number | undefined>;
let toBeDecorated: AsyncFnMock<typeof decorated>;
let logErrorMock: jest.Mock;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
loggerStub = {
error: jest.fn(),
} as unknown as Logger;
logErrorMock = jest.fn();
di.override(loggerInjectable, () => loggerStub);
di.override(logErrorInjectable, () => logErrorMock);
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
@@ -153,7 +148,7 @@ describe("with-error-logging", () => {
});
it("does not log error yet", () => {
expect(loggerStub.error).not.toHaveBeenCalled();
expect(logErrorMock).not.toHaveBeenCalled();
});
it("does not resolve yet", async () => {
@@ -176,7 +171,7 @@ describe("with-error-logging", () => {
error = e;
}
expect(loggerStub.error).toHaveBeenCalledWith("some-error-message-for-some-error", error);
expect(logErrorMock).toHaveBeenCalledWith("some-error-message-for-some-error", error);
});
it("rejects", () => {
@@ -198,7 +193,7 @@ describe("with-error-logging", () => {
});
it("logs the rejection", () => {
expect(loggerStub.error).toHaveBeenCalledWith(
expect(logErrorMock).toHaveBeenCalledWith(
"some-error-message-for-some-rejection",
error,
);
@@ -215,7 +210,7 @@ describe("with-error-logging", () => {
});
it("does not log error", () => {
expect(loggerStub.error).not.toHaveBeenCalled();
expect(logErrorMock).not.toHaveBeenCalled();
});
it("resolves with the value", async () => {
@@ -231,7 +226,7 @@ describe("with-error-logging", () => {
});
it("does not log error", () => {
expect(loggerStub.error).not.toHaveBeenCalled();
expect(logErrorMock).not.toHaveBeenCalled();
});
it("resolves without value", async () => {
@@ -5,21 +5,20 @@
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
import loggerInjectable from "../../logger.injectable";
import type { Logger } from "../../logger";
import withOrphanPromiseInjectable from "./with-orphan-promise.injectable";
import logErrorInjectable from "../../log-error.injectable";
describe("with orphan promise, when called", () => {
let toBeDecorated: AsyncFnMock<(arg1: string, arg2: string) => Promise<string>>;
let actual: void;
let loggerStub: Logger;
let logErrorMock: jest.Mock;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
loggerStub = { error: jest.fn() } as unknown as Logger;
logErrorMock = jest.fn();
di.override(loggerInjectable, () => loggerStub);
di.override(logErrorInjectable, () => logErrorMock);
const withOrphanPromise = di.inject(withOrphanPromiseInjectable);
@@ -49,7 +48,7 @@ describe("with orphan promise, when called", () => {
});
it("logs the rejection", () => {
expect(loggerStub.error).toHaveBeenCalledWith("Orphan promise rejection encountered", "some-error");
expect(logErrorMock).toHaveBeenCalledWith("Orphan promise rejection encountered", "some-error");
});
it("nothing else happens", () => {
+4 -1
View File
@@ -4,9 +4,12 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
// Todo: OCP by creating distinct injectables for platforms.
export const allPlatforms = ["win32", "darwin", "linux"] as const;
const platformInjectable = getInjectable({
id: "platform",
instantiate: () => process.platform,
instantiate: () => process.platform as typeof allPlatforms[number],
causesSideEffects: true,
});
@@ -2,9 +2,9 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ReleaseChannel } from "../application-update/update-channels";
import { createInitializableState } from "../initializable-state/create";
import buildSemanticVersionInjectable from "./build-semantic-version.injectable";
import type { ReleaseChannel } from "../../features/application-update/common/update-channels";
const releaseChannelInjectable = createInitializableState({
id: "release-channel",