1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx
Panu Horsmalahti c9418f6362
Allow specifying ResourceName in KubeObjectListLayout (#6926)
* Allow specifying ResourceName in KubeObjectListLayout

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Fix comment syntax.

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
2023-01-12 11:38:50 -05:00

196 lines
6.6 KiB
TypeScript

/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./kube-object-list-layout.scss";
import React from "react";
import { computed, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import type { Disposer } from "../../utils";
import { cssNames, isDefined } from "../../utils";
import type { KubeJsonApiDataFor, KubeObject } from "../../../common/k8s-api/kube-object";
import type { ItemListLayoutProps } from "../item-object-list/list-layout";
import { ItemListLayout } from "../item-object-list/list-layout";
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
import { KubeObjectMenu } from "../kube-object-menu";
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
import { ResourceKindMap, ResourceNames } from "../../utils/rbac";
import { Icon } from "../icon";
import { TooltipPosition } from "../tooltip";
import { withInjectables } from "@ogre-tools/injectable-react";
import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable";
import type { SubscribableStore, SubscribeStores } from "../../kube-watch-api/kube-watch-api";
import type { KubeApi } from "../../../common/k8s-api/kube-api";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import type { PageParam } from "../../navigation/page-param";
import type { ToggleKubeDetailsPane } from "../kube-detail-params/toggle-details.injectable";
import kubeSelectedUrlParamInjectable from "../kube-detail-params/kube-selected-url.injectable";
import toggleKubeDetailsPaneInjectable from "../kube-detail-params/toggle-details.injectable";
import type { ClusterContext } from "../../cluster-frame-context/cluster-frame-context";
export interface KubeObjectListLayoutProps<
K extends KubeObject,
A extends KubeApi<K, D>,
D extends KubeJsonApiDataFor<K>,
> extends Omit<ItemListLayoutProps<K, false>, "getItems" | "dependentStores" | "preloadStores"> {
items?: K[];
getItems?: () => K[];
store: KubeObjectStore<K, A, D>;
dependentStores?: SubscribableStore[];
subscribeStores?: boolean;
/**
* Customize resource name for e.g. search input ("Search <ResourceName>..."")
* If not provided, ResourceNames is used instead with a fallback to resource kind.
*/
resourceName?: string;
}
interface Dependencies {
clusterFrameContext: ClusterContext;
subscribeToStores: SubscribeStores;
kubeSelectedUrlParam: PageParam<string>;
toggleKubeDetailsPane: ToggleKubeDetailsPane;
}
const getLoadErrorMessage = (error: unknown): string => {
if (error instanceof Error) {
if (error.cause) {
return `${error.message}: ${getLoadErrorMessage(error.cause)}`;
}
return error.message;
}
return `${error}`;
};
@observer
class NonInjectedKubeObjectListLayout<
K extends KubeObject,
A extends KubeApi<K, D>,
D extends KubeJsonApiDataFor<K>,
> extends React.Component<KubeObjectListLayoutProps<K, A, D> & Dependencies> {
static defaultProps = {
subscribeStores: true,
};
private readonly loadErrors = observable.array<string>();
@computed get selectedItem() {
return this.props.store.getByPath(this.props.kubeSelectedUrlParam.get());
}
componentDidMount() {
const { store, dependentStores = [], subscribeStores } = this.props;
const stores = Array.from(new Set([store, ...dependentStores]));
const reactions: Disposer[] = [
reaction(() => this.props.clusterFrameContext.contextNamespaces.slice(), () => {
// clear load errors
this.loadErrors.length = 0;
}),
];
if (subscribeStores) {
reactions.push(
this.props.subscribeToStores(stores, {
onLoadFailure: error => {
this.loadErrors.push(getLoadErrorMessage(error));
},
}),
);
}
disposeOnUnmount(this, reactions);
}
renderLoadErrors() {
if (this.loadErrors.length === 0) {
return null;
}
return (
<Icon
material="warning"
className="load-error"
tooltip={{
children: (
<>
{this.loadErrors.map((error, index) => <p key={index}>{error}</p>)}
</>
),
preferredPositions: TooltipPosition.BOTTOM,
}}
/>
);
}
render() {
const {
className,
customizeHeader,
store,
items,
dependentStores,
toggleKubeDetailsPane: toggleDetails,
onDetails,
...layoutProps
} = this.props;
const resourceName = this.props.resourceName || ResourceNames[ResourceKindMap[store.api.kind]] || store.api.kind;
return (
<ItemListLayout<K, false>
className={cssNames("KubeObjectListLayout", className)}
store={store}
getItems={() => this.props.items || store.contextItems}
preloadStores={false} // loading handled in kubeWatchApi.subscribeStores()
detailsItem={this.selectedItem}
customizeHeader={[
({ filters, searchProps, info, ...headerPlaceHolders }) => ({
filters: (
<>
{filters}
{store.api.isNamespaced && <NamespaceSelectFilter id="kube-object-list-layout-namespace-select-input" />}
</>
),
searchProps: {
...searchProps,
placeholder: `Search ${resourceName}...`,
},
info: (
<>
{info}
{this.renderLoadErrors()}
</>
),
...headerPlaceHolders,
}),
...[customizeHeader].filter(isDefined).flat(),
]}
renderItemMenu={item => <KubeObjectMenu object={item} />}
onDetails={onDetails ?? ((item) => toggleDetails(item.selfLink))}
{...layoutProps}
/>
);
}
}
export const KubeObjectListLayout = withInjectables<
Dependencies,
KubeObjectListLayoutProps<KubeObject, KubeApi<KubeObject, KubeJsonApiDataFor<KubeObject>>, KubeJsonApiDataFor<KubeObject>>
>(NonInjectedKubeObjectListLayout, {
getProps: (di, props) => ({
...props,
clusterFrameContext: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
subscribeToStores: di.inject(subscribeStoresInjectable),
kubeSelectedUrlParam: di.inject(kubeSelectedUrlParamInjectable),
toggleKubeDetailsPane: di.inject(toggleKubeDetailsPaneInjectable),
}),
}) as <
K extends KubeObject,
A extends KubeApi<K, D>,
D extends KubeJsonApiDataFor<K> = KubeJsonApiDataFor<K>,
>(props: KubeObjectListLayoutProps<K, A, D>) => React.ReactElement;