From ae2fa15b20e3bdf56def5ddf4f060d2448e177e6 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 8 Jun 2021 07:41:36 -0400 Subject: [PATCH] Make customizing ItemListLayout header explicitly reducable (#2956) - Add search placeholder text for helm releases Signed-off-by: Sebastian Malton --- .../+apps-helm-charts/helm-charts.tsx | 10 ++- .../components/+apps-releases/releases.tsx | 8 +- src/renderer/components/+catalog/catalog.tsx | 2 - .../+custom-resources/crd-list.scss | 6 +- .../components/+custom-resources/crd-list.tsx | 46 ++++++------ .../+custom-resources/crd-resources.tsx | 7 ++ src/renderer/components/+events/events.tsx | 20 +++-- .../components/input/search-input-url.tsx | 6 +- .../item-object-list/item-list-layout.tsx | 73 ++++++++----------- .../kube-object/kube-object-list-layout.tsx | 21 +++++- src/renderer/utils/rbac.ts | 7 +- 11 files changed, 122 insertions(+), 84 deletions(-) diff --git a/src/renderer/components/+apps-helm-charts/helm-charts.tsx b/src/renderer/components/+apps-helm-charts/helm-charts.tsx index d8cb43a98d..0bf80577bd 100644 --- a/src/renderer/components/+apps-helm-charts/helm-charts.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-charts.tsx @@ -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 { (chart: HelmChart) => chart.getAppVersion(), (chart: HelmChart) => chart.getKeywords(), ]} - customizeHeader={() => ( - - )} + customizeHeader={({ searchProps }) => ({ + searchProps: { + ...searchProps, + placeholder: "Search Helm Charts...", + }, + })} renderTableHeader={[ { className: "icon", showWithColumn: columnId.name }, { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, diff --git a/src/renderer/components/+apps-releases/releases.tsx b/src/renderer/components/+apps-releases/releases.tsx index bdc8cf22b6..bb16a4e723 100644 --- a/src/renderer/components/+apps-releases/releases.tsx +++ b/src/renderer/components/+apps-releases/releases.tsx @@ -117,16 +117,20 @@ export class HelmReleases extends Component { (release: HelmRelease) => release.getStatus(), (release: HelmRelease) => release.getVersion(), ]} - renderHeaderTitle="Releases" - customizeHeader={({ filters, ...headerPlaceholders }) => ({ + customizeHeader={({ filters, searchProps, ...headerPlaceholders }) => ({ filters: ( <> {filters} ), + 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 }, diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index c2d180d2e2..2414d90a49 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -205,7 +205,6 @@ export class Catalog extends React.Component { return ( { return ( { + customizeHeader={({ filters, ...headerPlaceholders }) => { let placeholder = <>All groups; if (selectedGroups.length == 1) placeholder = <>Group: {selectedGroups[0]}; @@ -104,26 +104,30 @@ export class CrdList extends React.Component { return { // todo: move to global filters filters: ( - this.toggleSelection(group)} + closeMenuOnSelect={false} + controlShouldRenderValue={false} + formatOptionLabel={({ value: group }: SelectOption) => { + const isSelected = selectedGroups.includes(group); + + return ( +
+ + {group} + {isSelected && } +
+ ); + }} + /> + + ), + ...headerPlaceholders, }; }} renderTableHeader={[ diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index b42703aa33..76ea8607b5 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -101,6 +101,13 @@ export class CrdResources extends React.Component { (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 }, diff --git a/src/renderer/components/+events/events.tsx b/src/renderer/components/+events/events.tsx index adf7b2d105..aa0ba475a9 100644 --- a/src/renderer/components/+events/events.tsx +++ b/src/renderer/components/+events/events.tsx @@ -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 { 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} - ({visibleItems.length} of {items.length}) - ; + return { + title, + info: ({visibleItems.length} of {items.length}), + }; } return { @@ -136,7 +138,9 @@ export class Events extends React.Component { className="help-icon" tooltip={`Limited to ${store.limit}`} /> - + , + title, + ...headerPlaceholders }; }; diff --git a/src/renderer/components/input/search-input-url.tsx b/src/renderer/components/input/search-input-url.tsx index 131f17d383..328f189d6d 100644 --- a/src/renderer/components/input/search-input-url.tsx +++ b/src/renderer/components/input/search-input-url.tsx @@ -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 { +export class SearchInputUrl extends React.Component { @observable inputVal = ""; // fix: use empty string on init to avoid react warnings @disposeOnUnmount @@ -62,7 +62,7 @@ export class SearchInputUrl extends React.Component { } }; - constructor(props: Props) { + constructor(props: SearchInputUrlProps) { super(props); makeObservable(this); } diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx index 647cbcaec9..72dafbd90e 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -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 = (item: T) => string | number | (string | number)[]; export type ItemsFilter = (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 { tableId?: string; className: IClassName; @@ -72,12 +72,11 @@ export interface ItemListLayoutProps { showHeader?: boolean; headerClassName?: IClassName; renderHeaderTitle?: ReactNode | ((parent: ItemListLayout) => ReactNode); - customizeHeader?: (placeholders: IHeaderPlaceholders, content: ReactNode) => Partial | 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 { const defaultProps: Partial = { 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 { 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 { @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 { 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} -
+ { + info && ( +
+ {info} +
+ ) + } {filters} - {isSearchable && searchFilters && search} + {searchFilters.length > 0 && searchProps && } ); } @@ -385,28 +389,15 @@ export class ItemListLayout extends React.Component { 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:
{title}
, info: this.renderInfo(), - filters: showNamespaceSelectFilter && , - search: , + 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 (
diff --git a/src/renderer/components/kube-object/kube-object-list-layout.tsx b/src/renderer/components/kube-object/kube-object-list-layout.tsx index 52c886f6a2..39b0ee5019 100644 --- a/src/renderer/components/kube-object/kube-object-list-layout.tsx +++ b/src/renderer/components/kube-object/kube-object-list-layout.tsx @@ -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 ({ + filters: ( + <> + {filters} + {store.api.isNamespaced && } + + ), + searchProps: { + ...searchProps, + placeholder: `Search ${placeholderString}...`, + }, + ...headerPlaceHolders, + }), + ...[customizeHeader].flat(), + ]} renderItemMenu={(item: KubeObject) => } // safe because we are dealing with KubeObjects here /> ); diff --git a/src/renderer/utils/rbac.ts b/src/renderer/utils/rbac.ts index ca264f050c..da167541fc 100644 --- a/src/renderer/utils/rbac.ts +++ b/src/renderer/utils/rbac.ts @@ -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 = { "namespaces": "Namespaces", @@ -53,3 +53,8 @@ export const ResourceNames: Record = { "clusterroles": "Cluster Roles", "serviceaccounts": "Service Accounts" }; + +export const ResourceKindMap: Record = Object.fromEntries( + Object.entries(apiResourceRecord) + .map(([resource, { kind }]) => [kind, resource as KubeResource]) +);