1
0
mirror of https://github.com/lensapp/lens.git synced 2024-09-20 13:57:23 +03:00

Make customizing ItemListLayout header explicitly reducable (#2956)

- Add search placeholder text for helm releases

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-06-08 07:41:36 -04:00 committed by GitHub
parent 757fed47fa
commit ae2fa15b20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 122 additions and 84 deletions

View File

@ -30,7 +30,6 @@ import type { HelmChart } from "../../api/endpoints/helm-charts.api";
import { HelmChartDetails } from "./helm-chart-details";
import { navigation } from "../../navigation";
import { ItemListLayout } from "../item-object-list/item-list-layout";
import { SearchInputUrl } from "../input";
enum columnId {
name = "name",
@ -92,9 +91,12 @@ export class HelmCharts extends Component<Props> {
(chart: HelmChart) => chart.getAppVersion(),
(chart: HelmChart) => chart.getKeywords(),
]}
customizeHeader={() => (
<SearchInputUrl placeholder="Search Helm Charts" />
)}
customizeHeader={({ searchProps }) => ({
searchProps: {
...searchProps,
placeholder: "Search Helm Charts...",
},
})}
renderTableHeader={[
{ className: "icon", showWithColumn: columnId.name },
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },

View File

@ -117,16 +117,20 @@ export class HelmReleases extends Component<Props> {
(release: HelmRelease) => release.getStatus(),
(release: HelmRelease) => release.getVersion(),
]}
renderHeaderTitle="Releases"
customizeHeader={({ filters, ...headerPlaceholders }) => ({
customizeHeader={({ filters, searchProps, ...headerPlaceholders }) => ({
filters: (
<>
{filters}
<NamespaceSelectFilter />
</>
),
searchProps: {
...searchProps,
placeholder: "Search Releases...",
},
...headerPlaceholders,
})}
renderHeaderTitle="Releases"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },

View File

@ -205,7 +205,6 @@ export class Catalog extends React.Component<Props> {
return (
<ItemListLayout
renderHeaderTitle={this.catalogEntityStore.activeCategory?.metadata.name ?? "Browse All"}
isSearchable={true}
isSelectable={false}
className="CatalogItemList"
store={this.catalogEntityStore}
@ -242,7 +241,6 @@ export class Catalog extends React.Component<Props> {
return (
<ItemListLayout
renderHeaderTitle={this.catalogEntityStore.activeCategory?.metadata.name ?? "Browse All"}
isSearchable={true}
isSelectable={false}
className="CatalogItemList"
store={this.catalogEntityStore}

View File

@ -42,4 +42,8 @@
}
}
}
.SearchInput {
width: 300px;
}
}

View File

@ -95,7 +95,7 @@ export class CrdList extends React.Component {
sortingCallbacks={sortingCallbacks}
searchFilters={Object.values(sortingCallbacks)}
renderHeaderTitle="Custom Resources"
customizeHeader={() => {
customizeHeader={({ filters, ...headerPlaceholders }) => {
let placeholder = <>All groups</>;
if (selectedGroups.length == 1) placeholder = <>Group: {selectedGroups[0]}</>;
@ -104,6 +104,8 @@ export class CrdList extends React.Component {
return {
// todo: move to global filters
filters: (
<>
{filters}
<Select
className="group-select"
placeholder={placeholder}
@ -123,7 +125,9 @@ export class CrdList extends React.Component {
);
}}
/>
)
</>
),
...headerPlaceholders,
};
}}
renderTableHeader={[

View File

@ -101,6 +101,13 @@ export class CrdResources extends React.Component<Props> {
(item: KubeObject) => item.getSearchFields(),
]}
renderHeaderTitle={crd.getResourceTitle()}
customizeHeader={({ searchProps, ...headerPlaceholders }) => ({
searchProps: {
...searchProps,
placeholder: `Search ${crd.getResourceTitle()}...`,
},
...headerPlaceholders
})}
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
isNamespaced && { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },

View File

@ -30,7 +30,7 @@ import { EventStore, eventStore } from "./event.store";
import { getDetailsUrl, KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
import type { KubeEvent } from "../../api/endpoints/events.api";
import type { TableSortCallbacks, TableSortParams, TableProps } from "../table";
import type { IHeaderPlaceholders } from "../item-object-list";
import type { HeaderCustomizer } from "../item-object-list";
import { Tooltip } from "../tooltip";
import { Link } from "react-router-dom";
import { cssNames, IClassName, stopPropagation } from "../../utils";
@ -112,19 +112,21 @@ export class Events extends React.Component<Props> {
return this.items;
}
customizeHeader = ({ info, title }: IHeaderPlaceholders) => {
customizeHeader: HeaderCustomizer = ({ info, title, ...headerPlaceholders }) => {
const { compact } = this.props;
const { store, items, visibleItems } = this;
const allEventsAreShown = visibleItems.length === items.length;
// handle "compact"-mode header
if (compact) {
if (allEventsAreShown) return title; // title == "Events"
if (allEventsAreShown) {
return { title };
}
return <>
{title}
<span> ({visibleItems.length} of <Link to={eventsURL()}>{items.length}</Link>)</span>
</>;
return {
title,
info: <span> ({visibleItems.length} of <Link to={eventsURL()}>{items.length}</Link>)</span>,
};
}
return {
@ -136,7 +138,9 @@ export class Events extends React.Component<Props> {
className="help-icon"
tooltip={`Limited to ${store.limit}`}
/>
</>
</>,
title,
...headerPlaceholders
};
};

View File

@ -32,12 +32,12 @@ export const searchUrlParam = createPageParam({
defaultValue: "",
});
interface Props extends InputProps {
export interface SearchInputUrlProps extends InputProps {
compact?: boolean; // show only search-icon when not focused
}
@observer
export class SearchInputUrl extends React.Component<Props> {
export class SearchInputUrl extends React.Component<SearchInputUrlProps> {
@observable inputVal = ""; // fix: use empty string on init to avoid react warnings
@disposeOnUnmount
@ -62,7 +62,7 @@ export class SearchInputUrl extends React.Component<Props> {
}
};
constructor(props: Props) {
constructor(props: SearchInputUrlProps) {
super(props);
makeObservable(this);
}

View File

@ -32,7 +32,7 @@ import { AddRemoveButtons, AddRemoveButtonsProps } from "../add-remove-buttons";
import { NoItems } from "../no-items";
import { Spinner } from "../spinner";
import type { ItemObject, ItemStore } from "../../item.store";
import { SearchInputUrl } from "../input";
import { SearchInputUrlProps, SearchInputUrl } from "../input";
import { Filter, FilterType, pageFilters } from "./page-filters.store";
import { PageFiltersList } from "./page-filters-list";
import { ThemeStore } from "../../theme.store";
@ -41,21 +41,21 @@ import { MenuItem } from "../menu";
import { Checkbox } from "../checkbox";
import { UserStore } from "../../../common/user-store";
import { namespaceStore } from "../+namespaces/namespace.store";
import { KubeObjectStore } from "../../kube-object.store";
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
// todo: refactor, split to small re-usable components
export type SearchFilter<T extends ItemObject = any> = (item: T) => string | number | (string | number)[];
export type ItemsFilter<T extends ItemObject = any> = (items: T[]) => T[];
export interface IHeaderPlaceholders {
title: ReactNode;
search: ReactNode;
filters: ReactNode;
info: ReactNode;
export interface HeaderPlaceholders {
title?: ReactNode;
searchProps?: SearchInputUrlProps;
filters?: ReactNode;
info?: ReactNode;
}
export type HeaderCustomizer = (placeholders: HeaderPlaceholders) => HeaderPlaceholders;
export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
tableId?: string;
className: IClassName;
@ -72,12 +72,11 @@ export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
showHeader?: boolean;
headerClassName?: IClassName;
renderHeaderTitle?: ReactNode | ((parent: ItemListLayout) => ReactNode);
customizeHeader?: (placeholders: IHeaderPlaceholders, content: ReactNode) => Partial<IHeaderPlaceholders> | ReactNode;
customizeHeader?: HeaderCustomizer | HeaderCustomizer[];
// items list configuration
isReady?: boolean; // show loading indicator while not ready
isSelectable?: boolean; // show checkbox in rows for selecting items
isSearchable?: boolean; // apply search-filter & add search-input
isConfigurable?: boolean;
copyClassNameFromHeadCells?: boolean;
sortingCallbacks?: { [sortBy: string]: TableSortCallback };
@ -101,12 +100,13 @@ export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
const defaultProps: Partial<ItemListLayoutProps> = {
showHeader: true,
isSearchable: true,
isSelectable: true,
isConfigurable: false,
copyClassNameFromHeadCells: true,
preloadStores: true,
dependentStores: [],
searchFilters: [],
customizeHeader: [],
filterItems: [],
hasDetailsView: true,
onDetails: noop,
@ -160,10 +160,10 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
private filterCallbacks: { [type: string]: ItemsFilter } = {
[FilterType.SEARCH]: items => {
const { searchFilters, isSearchable } = this.props;
const { searchFilters } = this.props;
const search = pageFilters.getValues(FilterType.SEARCH)[0] || "";
if (search && isSearchable && searchFilters) {
if (search && searchFilters.length) {
const normalizeText = (text: string) => String(text).toLowerCase();
const searchTexts = [search].map(normalizeText);
@ -190,9 +190,9 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
@computed get filters() {
let { activeFilters } = pageFilters;
const { isSearchable, searchFilters } = this.props;
const { searchFilters } = this.props;
if (!(isSearchable && searchFilters)) {
if (searchFilters.length === 0) {
activeFilters = activeFilters.filter(({ type }) => type !== FilterType.SEARCH);
}
@ -348,18 +348,22 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
return this.items.map(item => this.getRow(item.getId()));
}
renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode {
const { isSearchable, searchFilters } = this.props;
const { title, filters, search, info } = placeholders;
renderHeaderContent(placeholders: HeaderPlaceholders): ReactNode {
const { searchFilters } = this.props;
const { title, filters, searchProps, info } = placeholders;
return (
<>
{title}
{
info && (
<div className="info-panel box grow">
{info}
</div>
)
}
{filters}
{isSearchable && searchFilters && search}
{searchFilters.length > 0 && searchProps && <SearchInputUrl {...searchProps} />}
</>
);
}
@ -385,28 +389,15 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
return null;
}
const showNamespaceSelectFilter = this.props.store instanceof KubeObjectStore && this.props.store.api.isNamespaced;
const title = typeof renderHeaderTitle === "function" ? renderHeaderTitle(this) : renderHeaderTitle;
const placeholders: IHeaderPlaceholders = {
const customizeHeaders = [customizeHeader].flat().filter(Boolean);
const initialPlaceholders: HeaderPlaceholders = {
title: <h5 className="title">{title}</h5>,
info: this.renderInfo(),
filters: showNamespaceSelectFilter && <NamespaceSelectFilter />,
search: <SearchInputUrl />,
searchProps: {},
};
let header = this.renderHeaderContent(placeholders);
if (customizeHeader) {
const modifiedHeader = customizeHeader(placeholders, header) ?? {};
if (isReactNode(modifiedHeader)) {
header = modifiedHeader;
} else {
header = this.renderHeaderContent({
...placeholders,
...modifiedHeader as IHeaderPlaceholders,
});
}
}
const headerPlaceholders = customizeHeaders.reduce((prevPlaceholders, customizer) => customizer(prevPlaceholders), initialPlaceholders);
const header = this.renderHeaderContent(headerPlaceholders);
return (
<div className={cssNames("header flex gaps align-center", headerClassName)}>

View File

@ -30,6 +30,8 @@ import { KubeObjectMenu } from "./kube-object-menu";
import { kubeSelectedUrlParam, showDetails } from "./kube-object-details";
import { kubeWatchApi } from "../../api/kube-watch-api";
import { clusterContext } from "../context";
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
import { ResourceKindMap, ResourceNames } from "../../utils/rbac";
export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
store: KubeObjectStore;
@ -66,7 +68,8 @@ export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutPr
}
render() {
const { className, store, items = store.contextItems, ...layoutProps } = this.props;
const { className, customizeHeader, store, items = store.contextItems, ...layoutProps } = this.props;
const placeholderString = ResourceNames[ResourceKindMap[store.api.kind]] || store.api.kind;
return (
<ItemListLayout
@ -76,6 +79,22 @@ export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutPr
items={items}
preloadStores={false} // loading handled in kubeWatchApi.subscribeStores()
detailsItem={this.selectedItem}
customizeHeader={[
({ filters, searchProps, ...headerPlaceHolders }) => ({
filters: (
<>
{filters}
{store.api.isNamespaced && <NamespaceSelectFilter />}
</>
),
searchProps: {
...searchProps,
placeholder: `Search ${placeholderString}...`,
},
...headerPlaceHolders,
}),
...[customizeHeader].flat(),
]}
renderItemMenu={(item: KubeObject) => <KubeObjectMenu object={item} />} // safe because we are dealing with KubeObjects here
/>
);

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { KubeResource } from "../../common/rbac";
import { apiResourceRecord, KubeResource } from "../../common/rbac";
export const ResourceNames: Record<KubeResource, string> = {
"namespaces": "Namespaces",
@ -53,3 +53,8 @@ export const ResourceNames: Record<KubeResource, string> = {
"clusterroles": "Cluster Roles",
"serviceaccounts": "Service Accounts"
};
export const ResourceKindMap: Record<string, KubeResource> = Object.fromEntries(
Object.entries(apiResourceRecord)
.map(([resource, { kind }]) => [kind, resource as KubeResource])
);