Technical requirements for behavioural unit tests (#5084)

* Implement a lot of technical requirements for behavioural unit tests

Note: the crux of this was to make routing env-agnostic, and not based on URLs as magic strings, but instead something type-enforced.

Note: extension-based routes comply to same exact interface by "late-registering" their routes when installed. Routes are just injectables.

Note: another chunk of global shared state is no more.

Note: a lot of explicit side effects have been cornered to injectables.

Note: a lot of stuff has become reactive as part if this.

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

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

* Make a directory commonly available

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

* Require id for <Select /> to prevent non-deterministic renders

This was caused by global state in a 3rd party lib: "react-select".

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

* Specify id for all <Select /> to satisfy previous commit

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

* Prevent explicit side effect in component by using existing dependency instead

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

* Extract instantiation of "conf" as injectables for causing side effects

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

* Introduce a legacy-helper to make gradual refactoring of inheritors of Singleton easier

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

* Make legacy unit tests for hotbar green and more simple by using the new legacy helper

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

* Temporarily kludge all unit tests green with a disclaimer about allowing side-effects

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

* Remove kludge in previous commit by explicitly permitting specific side effects where old unit tests require it

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

* Prevent old unit test with side effects from accessing file system

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

* Migrate to actual typing for di.permitSideEffects

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

* Prevent unit tests from failing because of non-standard method of HTML-element not present in js-dom

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

* Adapt integration tests to recent changes

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

* Fix code style

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

* Fix artifact from bad rebase

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

* Add a deprecation from a review comment

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

* Remove change that is not required

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

* Remove redundant comment

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

* Fix code style

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

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

* Remove redundant file

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

* Fix bad merge

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

* Improve variable name

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

* Tweak logger interface to be more descriptive

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

* Make injecting legacy singleton always provide new instance

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

* Remove conditional typing when not needed

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

* Improve naming of variable

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

* Remove unnecessary code style changes

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

* Remove flag for causing side effects from too broad scope

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

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

* Override side-effects in unit test using injectable instead of monkey patching

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

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

* Flag some side-effects and add general overrides

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

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

* Fix unit tests in CI by removing explicit side-effect

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

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

* Remove explicit side-effect from getting default shell

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

* Introduce abstraction for getting absolute paths

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

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

* Switch to using abstraction for getting absolute path to control explicit side effect

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

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

* Introduce abstraction for joining paths

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

* Switch to using abstraction for joining paths to control explicit side effect

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

* Fix fake implementation for join paths

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

* Fix test after removing explicit side effect

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

* Remove explicit side effects from kubeconfig-syncs

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

* Fix arguments after removing explicit side effect

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

* Make registrators not async for not being needed anymore

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

* Make generalCatalogEntities non-observable, as there is no requirement

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

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

* Remove redundant code

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

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

* Simplify logic for registering general catalog entity sources

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

* Add TODO

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

* Replace function for getting application menu items with reactive solution

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

* Fix typo in interface name

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

* Remove global shared state usages of hot bar store

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

* Remove redundant enum

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-03-31 16:57:05 +03:00
1 parent ab2cd27a27
commit a277cfcf02
583 files changed
+13073 -6556

No files matched your search

@@ -0,0 +1,21 @@
/**
* 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 crdListRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/crd-list/crd-list-route.injectable";
import { CustomResourceDefinitions } from "./crd-list";
const crdListRouteComponentInjectable = getInjectable({
id: "crd-list-route-component",
instantiate: (di) => ({
route: di.inject(crdListRouteInjectable),
Component: CustomResourceDefinitions,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default crdListRouteComponentInjectable;
@@ -16,6 +16,7 @@ import { Select, SelectOption } from "../select";
import { createPageParam } from "../../navigation";
import { Icon } from "../icon";
import { KubeObjectAge } from "../kube-object/age";
import { TabLayout } from "../layout/tab-layout-2";
export const crdGroupsUrlParam = createPageParam<string[]>({
name: "groups",
@@ -64,81 +65,84 @@ export class CustomResourceDefinitions extends React.Component {
const { items, selectedGroups } = this;
return (
<KubeObjectListLayout
isConfigurable
tableId="crd"
className="CrdList"
store={crdStore}
// Don't subscribe the `crdStore` because <Sidebar> already has and is always mounted
subscribeStores={false}
items={items}
sortingCallbacks={{
[columnId.kind]: crd => crd.getResourceKind(),
[columnId.group]: crd => crd.getGroup(),
[columnId.version]: crd => crd.getVersion(),
[columnId.scope]: crd => crd.getScope(),
[columnId.age]: crd => -crd.getCreationTimestamp(),
}}
searchFilters={[
crd => crd.getResourceKind(),
crd => crd.getGroup(),
crd => crd.getVersion(),
crd => crd.getScope(),
crd => -crd.getCreationTimestamp(),
]}
renderHeaderTitle="Custom Resources"
customizeHeader={({ filters, ...headerPlaceholders }) => {
let placeholder = <>All groups</>;
<TabLayout>
<KubeObjectListLayout
isConfigurable
tableId="crd"
className="CrdList"
store={crdStore}
// Don't subscribe the `crdStore` because <Sidebar> already has and is always mounted
subscribeStores={false}
items={items}
sortingCallbacks={{
[columnId.kind]: crd => crd.getResourceKind(),
[columnId.group]: crd => crd.getGroup(),
[columnId.version]: crd => crd.getVersion(),
[columnId.scope]: crd => crd.getScope(),
[columnId.age]: crd => -crd.getCreationTimestamp(),
}}
searchFilters={[
crd => crd.getResourceKind(),
crd => crd.getGroup(),
crd => crd.getVersion(),
crd => crd.getScope(),
crd => -crd.getCreationTimestamp(),
]}
renderHeaderTitle="Custom Resources"
customizeHeader={({ filters, ...headerPlaceholders }) => {
let placeholder = <>All groups</>;
if (selectedGroups.length == 1) placeholder = <>Group: {selectedGroups[0]}</>;
if (selectedGroups.length >= 2) placeholder = <>Groups: {selectedGroups.join(", ")}</>;
if (selectedGroups.length == 1) placeholder = <>Group: {selectedGroups[0]}</>;
if (selectedGroups.length >= 2) placeholder = <>Groups: {selectedGroups.join(", ")}</>;
return {
// todo: move to global filters
filters: (
<>
{filters}
<Select
className="group-select"
placeholder={placeholder}
options={Object.keys(crdStore.groups)}
onChange={({ value: group }: SelectOption) => this.toggleSelection(group)}
closeMenuOnSelect={false}
controlShouldRenderValue={false}
formatOptionLabel={({ value: group }: SelectOption) => {
const isSelected = selectedGroups.includes(group);
return {
// todo: move to global filters
filters: (
<>
{filters}
<Select
id="crd-input"
className="group-select"
placeholder={placeholder}
options={Object.keys(crdStore.groups)}
onChange={({ value: group }: SelectOption) => this.toggleSelection(group)}
closeMenuOnSelect={false}
controlShouldRenderValue={false}
formatOptionLabel={({ value: group }: SelectOption) => {
const isSelected = selectedGroups.includes(group);
return (
<div className="flex gaps align-center">
<Icon small material="folder"/>
<span>{group}</span>
{isSelected && <Icon small material="check" className="box right"/>}
</div>
);
}}
/>
</>
),
...headerPlaceholders,
};
}}
renderTableHeader={[
{ title: "Resource", className: "kind", sortBy: columnId.kind, id: columnId.kind },
{ title: "Group", className: "group", sortBy: columnId.group, id: columnId.group },
{ title: "Version", className: "version", sortBy: columnId.version, id: columnId.version },
{ title: "Scope", className: "scope", sortBy: columnId.scope, id: columnId.scope },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={crd => [
<Link key="link" to={crd.getResourceUrl()} onClick={stopPropagation}>
{crd.getResourceKind()}
</Link>,
crd.getGroup(),
crd.getVersion(),
crd.getScope(),
<KubeObjectAge key="age" object={crd} />,
]}
/>
return (
<div className="flex gaps align-center">
<Icon small material="folder"/>
<span>{group}</span>
{isSelected && <Icon small material="check" className="box right"/>}
</div>
);
}}
/>
</>
),
...headerPlaceholders,
};
}}
renderTableHeader={[
{ title: "Resource", className: "kind", sortBy: columnId.kind, id: columnId.kind },
{ title: "Group", className: "group", sortBy: columnId.group, id: columnId.group },
{ title: "Version", className: "version", sortBy: columnId.version, id: columnId.version },
{ title: "Scope", className: "scope", sortBy: columnId.scope, id: columnId.scope },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={crd => [
<Link key="link" to={crd.getResourceUrl()} onClick={stopPropagation}>
{crd.getResourceKind()}
</Link>,
crd.getGroup(),
crd.getVersion(),
crd.getScope(),
<KubeObjectAge key="age" object={crd} />,
]}
/>
</TabLayout>
);
}
}
@@ -8,35 +8,36 @@ import "./crd-resources.scss";
import React from "react";
import { value } from "jsonpath";
import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object-list-layout";
import { computed, makeObservable } from "mobx";
import { computed, IComputedValue, makeObservable } from "mobx";
import { crdStore } from "./crd.store";
import { apiManager } from "../../../common/k8s-api/api-manager";
import { parseJsonPath } from "../../utils/jsonPath";
import type { CRDRouteParams } from "../../../common/routes";
import { TabLayout } from "../layout/tab-layout-2";
import { withInjectables } from "@ogre-tools/injectable-react";
import customResourcesRouteParametersInjectable from "./custom-resources-route-parameters.injectable";
import { KubeObjectAge } from "../kube-object/age";
export interface CustomResourceDefinitionResourcesProps extends RouteComponentProps<CRDRouteParams> {
}
enum columnId {
name = "name",
namespace = "namespace",
age = "age",
}
interface Dependencies {
group: IComputedValue<string>;
name: IComputedValue<string>;
}
@observer
export class CustomResourceDefinitionResources extends React.Component<CustomResourceDefinitionResourcesProps> {
constructor(props: CustomResourceDefinitionResourcesProps) {
class NonInjectedCrdResources extends React.Component<Dependencies> {
constructor(props: Dependencies) {
super(props);
makeObservable(this);
}
@computed get crd() {
const { group, name } = this.props.match.params;
return crdStore.getByGroup(group, name);
return crdStore.getByGroup(this.props.group.get(), this.props.name.get());
}
@computed get store() {
@@ -55,69 +56,87 @@ export class CustomResourceDefinitionResources extends React.Component<CustomRes
const version = crd.getPreferedVersion();
return (
<KubeObjectListLayout
isConfigurable
key={`crd_resources_${crd.getResourceApiBase()}`}
tableId="crd_resources"
className="CrdResources"
store={store}
sortingCallbacks={{
[columnId.name]: customResource => customResource.getName(),
[columnId.namespace]: customResource => customResource.getNs(),
[columnId.age]: customResource => -customResource.getCreationTimestamp(),
...Object.fromEntries(extraColumns.map(({ name, jsonPath }) => [
name,
customResource => value(customResource, parseJsonPath(jsonPath.slice(1))),
])),
}}
searchFilters={[
customResource => customResource.getSearchFields(),
]}
renderHeaderTitle={crd.getResourceKind()}
customizeHeader={({ searchProps, ...headerPlaceholders }) => ({
searchProps: {
...searchProps,
placeholder: `${crd.getResourceKind()} search ...`,
},
...headerPlaceholders,
})}
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
isNamespaced && { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
...extraColumns.map(({ name }) => ({
title: name,
className: name.toLowerCase(),
sortBy: name,
id: name,
})),
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={crdInstance => [
crdInstance.getName(),
isNamespaced && crdInstance.getNs(),
...extraColumns.map((column) => {
let rawValue = value(crdInstance, parseJsonPath(column.jsonPath.slice(1)));
<TabLayout>
<KubeObjectListLayout
isConfigurable
key={`crd_resources_${crd.getResourceApiBase()}`}
tableId="crd_resources"
className="CrdResources"
store={store}
sortingCallbacks={{
[columnId.name]: customResource => customResource.getName(),
[columnId.namespace]: customResource => customResource.getNs(),
[columnId.age]: customResource => -customResource.getCreationTimestamp(),
...Object.fromEntries(extraColumns.map(({ name, jsonPath }) => [
name,
customResource => value(customResource, parseJsonPath(jsonPath.slice(1))),
])),
}}
searchFilters={[
customResource => customResource.getSearchFields(),
]}
renderHeaderTitle={crd.getResourceKind()}
customizeHeader={({ searchProps, ...headerPlaceholders }) => ({
searchProps: {
...searchProps,
placeholder: `${crd.getResourceKind()} search ...`,
},
...headerPlaceholders,
})}
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
isNamespaced && { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
...extraColumns.map(({ name }) => ({
title: name,
className: name.toLowerCase(),
sortBy: name,
id: name,
})),
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={crdInstance => [
crdInstance.getName(),
isNamespaced && crdInstance.getNs(),
...extraColumns.map((column) => {
let rawValue = value(crdInstance, parseJsonPath(column.jsonPath.slice(1)));
if (Array.isArray(rawValue) || typeof rawValue === "object") {
rawValue = JSON.stringify(rawValue);
}
if (Array.isArray(rawValue) || typeof rawValue === "object") {
rawValue = JSON.stringify(rawValue);
}
return {
renderBoolean: true,
children: rawValue,
};
}),
<KubeObjectAge key="age" object={crdInstance} />,
]}
failedToLoadMessage={(
<>
<p>Failed to load {crd.getPluralName()}</p>
{!version.served && (
<p>Prefered version ({crd.getGroup()}/{version.name}) is not served</p>
)}
</>
)}
/>
return {
renderBoolean: true,
children: rawValue,
};
}),
<KubeObjectAge key="age" object={crdInstance} />,
]}
failedToLoadMessage={(
<>
<p>Failed to load {crd.getPluralName()}</p>
{!version.served && (
<p>Prefered version ({crd.getGroup()}/{version.name}) is not served</p>
)}
</>
)}
/>
</TabLayout>
);
}
}
export const CrdResources = withInjectables<Dependencies>(
NonInjectedCrdResources,
{
getProps: (di) => {
const routeParameters = di.inject(customResourcesRouteParametersInjectable);
return {
group: routeParameters.group,
name: routeParameters.name,
};
},
},
);
@@ -0,0 +1,68 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { noop, some } from "lodash/fp";
import { computed } from "mobx";
import {
SidebarItemRegistration,
sidebarItemsInjectionToken,
} from "../layout/sidebar-items.injectable";
import { Icon } from "../icon";
import React from "react";
import crdListRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/crd-list/crd-list-route.injectable";
import sidebarItemsForDefinitionGroupsInjectable from "./sidebar-items-for-definition-groups.injectable";
import routeIsActiveInjectable from "../../routes/route-is-active.injectable";
import navigateToCrdListInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/crd-list/navigate-to-crd-list.injectable";
const customResourceSidebarItemsInjectable = getInjectable({
id: "custom-resource-sidebar-items",
instantiate: (di) => {
const navigateToCrdList = di.inject(navigateToCrdListInjectable);
const crdListRoute = di.inject(crdListRouteInjectable);
const crdListRouteIsActive = di.inject(routeIsActiveInjectable, crdListRoute);
const definitionGroupSidebarItems = di.inject(
sidebarItemsForDefinitionGroupsInjectable,
);
return computed((): SidebarItemRegistration[] => {
const definitionsItem = {
id: "definitions",
parentId: "custom-resources",
title: "Definitions",
onClick: navigateToCrdList,
isActive: crdListRouteIsActive,
isVisible: crdListRoute.isEnabled,
orderNumber: 10,
};
const definitionGroupItems = definitionGroupSidebarItems.get();
const childrenAndGrandChildren = [
definitionsItem,
...definitionGroupItems,
];
const parentItem: SidebarItemRegistration = {
id: "custom-resources",
parentId: null,
title: "Custom Resources",
getIcon: () => <Icon material="extension" />,
onClick: noop,
isVisible: computed(() => some(item => item.isVisible.get(), childrenAndGrandChildren)),
orderNumber: 110,
};
return [parentItem, definitionsItem, ...definitionGroupItems];
});
},
injectionToken: sidebarItemsInjectionToken,
});
export default customResourceSidebarItemsInjectable;
@@ -0,0 +1,21 @@
/**
* 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 { CrdResources } from "./crd-resources";
import customResourcesRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable";
import { routeSpecificComponentInjectionToken } from "../../routes/route-specific-component-injection-token";
const customResourcesRouteComponentInjectable = getInjectable({
id: "custom-resources-route-component",
instantiate: (di) => ({
route: di.inject(customResourcesRouteInjectable),
Component: CrdResources,
}),
injectionToken: routeSpecificComponentInjectionToken,
});
export default customResourcesRouteComponentInjectable;
@@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import routePathParametersInjectable from "../../routes/route-path-parameters.injectable";
import customResourcesRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable";
const customResourcesRouteParametersInjectable = getInjectable({
id: "custom-resources-route-parameters",
instantiate: (di) => {
const route = di.inject(customResourcesRouteInjectable);
const pathParameters = di.inject(routePathParametersInjectable, route);
return {
group: computed(() => pathParameters.get().group),
name: computed(() => pathParameters.get().name),
};
},
});
export default customResourcesRouteParametersInjectable;
@@ -6,11 +6,23 @@
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import { crdStore } from "./crd.store";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import currentlyInClusterFrameInjectable from "../../routes/currently-in-cluster-frame.injectable";
const customResourceDefinitionsInjectable = getInjectable({
id: "custom-resource-definitions",
instantiate: () => computed(() => [...crdStore.items]),
instantiate: (di) => {
const currentlyInClusterFrame = di.inject(currentlyInClusterFrameInjectable);
if (currentlyInClusterFrame) {
const subscribeStores = di.inject(subscribeStoresInjectable);
subscribeStores([crdStore]);
}
return computed(() => [...crdStore.items]);
},
});
export default customResourceDefinitionsInjectable;
@@ -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, IComputedValue } from "mobx";
import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints";
import { getOrInsert } from "../../utils";
import customResourceDefinitionsInjectable from "./custom-resources.injectable";
interface Dependencies {
definitions: IComputedValue<CustomResourceDefinition[]>;
}
function getGroupedCustomResourceDefinitions({ definitions }: Dependencies) {
return computed(() => {
const groups = new Map<string, CustomResourceDefinition[]>();
for (const crd of definitions.get()) {
getOrInsert(groups, crd.getGroup(), []).push(crd);
}
return groups;
});
}
const groupedCustomResourceDefinitionsInjectable = getInjectable({
id: "grouped-custom-resource-definitions",
instantiate: (di) => getGroupedCustomResourceDefinitions({
definitions: di.inject(customResourceDefinitionsInjectable),
}),
});
export default groupedCustomResourceDefinitionsInjectable;
@@ -1,61 +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, IComputedValue } from "mobx";
import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints";
import { crdURL, crdDefinitionsRoute } from "../../../common/routes";
import type { TabLayoutRoute } from "../layout/tab-layout";
import groupedCustomResourceDefinitionsInjectable from "./grouped-custom-resources.injectable";
export interface CustomResourceTabLayoutRoute extends Omit<TabLayoutRoute, "component"> {
id: string;
}
export interface CustomResourceGroupTabLayoutRoute extends CustomResourceTabLayoutRoute {
subRoutes?: CustomResourceTabLayoutRoute[];
}
interface Dependencies {
customResourcesDefinitions: IComputedValue<Map<string, CustomResourceDefinition[]>>;
}
function getRouteTabs({ customResourcesDefinitions }: Dependencies) {
return computed(() => {
const tabs: CustomResourceGroupTabLayoutRoute[] = [
{
id: "definitions",
title: "Definitions",
url: crdURL(),
routePath: String(crdDefinitionsRoute.path),
exact: true,
},
];
for (const [group, definitions] of customResourcesDefinitions.get()) {
tabs.push({
id: `crd-group:${group}`,
title: group,
routePath: crdURL({ query: { groups: group }}),
subRoutes: definitions.map(crd => ({
id: `crd-resource:${crd.getResourceApiBase()}`,
title: crd.getResourceKind(),
routePath: crd.getResourceUrl(),
})),
});
}
return tabs;
});
}
const customResourcesRouteTabsInjectable = getInjectable({
id: "custom-resources-route-tabs",
instantiate: (di) => getRouteTabs({
customResourcesDefinitions: di.inject(groupedCustomResourceDefinitionsInjectable),
}),
});
export default customResourcesRouteTabsInjectable;
@@ -1,22 +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 { Redirect, Route, Switch } from "react-router";
import { TabLayout } from "../layout/tab-layout";
import { crdDefinitionsRoute, crdResourcesRoute, crdURL } from "../../../common/routes";
import { CustomResourceDefinitions } from "./crd-list";
import { CustomResourceDefinitionResources } from "./crd-resources";
export const CustomResourcesRoute = () => (
<TabLayout>
<Switch>
<Route component={CustomResourceDefinitions} {...crdDefinitionsRoute} exact/>
<Route component={CustomResourceDefinitionResources} {...crdResourcesRoute}/>
<Redirect to={crdURL()}/>
</Switch>
</TabLayout>
);
@@ -1,76 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React, { useEffect } from "react";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { IComputedValue } from "mobx";
import { observer } from "mobx-react";
import customResourcesRouteTabsInjectable, { type CustomResourceGroupTabLayoutRoute } from "./route-tabs.injectable";
import type { IsAllowedResource } from "../../../common/utils/is-allowed-resource.injectable";
import isAllowedResourceInjectable from "../../../common/utils/is-allowed-resource.injectable";
import { crdURL, crdRoute } from "../../../common/routes";
import { isActiveRoute } from "../../navigation";
import { Icon } from "../icon";
import { SidebarItem } from "../layout/sidebar-item";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import type { SubscribeStores } from "../../kube-watch-api/kube-watch-api";
import { crdStore } from "./crd.store";
import { Spinner } from "../spinner";
export interface CustomResourcesSidebarItemProps {}
interface Dependencies {
routes: IComputedValue<CustomResourceGroupTabLayoutRoute[]>;
isAllowedResource: IsAllowedResource;
subscribeStores: SubscribeStores;
}
const NonInjectedCustomResourcesSidebarItem = observer(({ routes, isAllowedResource, subscribeStores }: Dependencies & CustomResourcesSidebarItemProps) => {
useEffect(() => subscribeStores([
crdStore,
]), []);
return (
<SidebarItem
id="custom-resources"
text="Custom Resources"
url={crdURL()}
isActive={isActiveRoute(crdRoute)}
isHidden={!isAllowedResource("customresourcedefinitions")}
icon={<Icon material="extension"/>}
>
{routes.get().map((route) => (
<SidebarItem
key={route.id}
id={route.id}
text={route.title}
url={route.routePath}
>
{route.subRoutes?.map((subRoute) => (
<SidebarItem
key={subRoute.id}
id={subRoute.id}
url={subRoute.routePath}
text={subRoute.title}
/>
))}
</SidebarItem>
))}
{crdStore.isLoading && (
<div className="flex justify-center">
<Spinner />
</div>
)}
</SidebarItem>
);
});
export const CustomResourcesSidebarItem = withInjectables<Dependencies, CustomResourcesSidebarItemProps>(NonInjectedCustomResourcesSidebarItem, {
getProps: (di, props) => ({
routes: di.inject(customResourcesRouteTabsInjectable),
isAllowedResource: di.inject(isAllowedResourceInjectable),
subscribeStores: di.inject(subscribeStoresInjectable),
...props,
}),
});
@@ -0,0 +1,81 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import crdListRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/crd-list/crd-list-route.injectable";
import customResourceDefinitionsInjectable from "./custom-resources.injectable";
import { groupBy, matches, noop, some, toPairs } from "lodash/fp";
import customResourcesRouteInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/custom-resources/custom-resources-route.injectable";
import currentPathParametersInjectable from "../../routes/current-path-parameters.injectable";
import type { SidebarItemRegistration } from "../layout/sidebar-items.injectable";
import routeIsActiveInjectable from "../../routes/route-is-active.injectable";
import navigateToCustomResourcesInjectable from "../../../common/front-end-routing/routes/cluster/custom-resources/custom-resources/navigate-to-custom-resources.injectable";
const sidebarItemsForDefinitionGroupsInjectable = getInjectable({
id: "sidebar-items-for-definition-groups",
instantiate: (di) => {
const customResourceDefinitions = di.inject(
customResourceDefinitionsInjectable,
);
const crdRoute = di.inject(customResourcesRouteInjectable);
const crdRouteIsActive = di.inject(routeIsActiveInjectable, crdRoute);
const crdListRoute = di.inject(crdListRouteInjectable);
const pathParameters = di.inject(currentPathParametersInjectable);
const navigateToCustomResources = di.inject(navigateToCustomResourcesInjectable);
return computed((): SidebarItemRegistration[] => {
const definitions = customResourceDefinitions.get();
const groupedCrds = toPairs(
groupBy((crd) => crd.getGroup(), definitions),
);
return groupedCrds.flatMap(([group, definitions]) => {
const childItems = definitions.map((crd) => {
const title = crd.getResourceKind();
const crdPathParameters = {
group: crd.getGroup(),
name: crd.getPluralName(),
};
return {
id: `custom-resource-definition-group-${group}-crd-${crd.getId()}`,
parentId: `custom-resource-definition-group-${group}`,
title,
onClick: () => navigateToCustomResources(crdPathParameters),
isActive: computed(
() =>
crdRouteIsActive.get() &&
matches(crdPathParameters, pathParameters.get()),
),
isVisible: crdListRoute.isEnabled,
orderNumber: 10,
};
});
return [
{
id: `custom-resource-definition-group-${group}`,
parentId: "custom-resources",
title: group,
onClick: noop,
isVisible: computed(() => some(item => item.isVisible.get(), childItems)),
orderNumber: 10,
},
...childItems,
];
});
});
},
});
export default sidebarItemsForDefinitionGroupsInjectable;