1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Column filters (#1532)

* Column filters

Signed-off-by: Pavel Ashevskii <pashevskii@mirantis.com>

* Add showWithColumn property

Signed-off-by: Pavel Ashevskii <pashevskii@mirantis.com>
This commit is contained in:
pashevskii 2021-01-13 11:38:20 +04:00 committed by GitHub
parent 11595abc93
commit 12c538b0eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 95 additions and 9 deletions

View File

@ -28,6 +28,7 @@ export interface UserPreferences {
downloadBinariesPath?: string; downloadBinariesPath?: string;
kubectlBinariesPath?: string; kubectlBinariesPath?: string;
openAtLogin?: boolean; openAtLogin?: boolean;
hiddenTableColumns?: Record<string, string[]>
} }
export class UserStore extends BaseStore<UserStoreModel> { export class UserStore extends BaseStore<UserStoreModel> {
@ -54,6 +55,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
downloadMirror: "default", downloadMirror: "default",
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
openAtLogin: false, openAtLogin: false,
hiddenTableColumns: {},
}; };
protected async handleOnLoad() { protected async handleOnLoad() {

View File

@ -74,6 +74,8 @@ export class Pods extends React.Component<Props> {
<KubeObjectListLayout <KubeObjectListLayout
className="Pods" store={podsStore} className="Pods" store={podsStore}
dependentStores={[volumeClaimStore, eventStore]} dependentStores={[volumeClaimStore, eventStore]}
tableId = "workloads_pods"
isConfigurable
sortingCallbacks={{ sortingCallbacks={{
[sortBy.name]: (pod: Pod) => pod.getName(), [sortBy.name]: (pod: Pod) => pod.getName(),
[sortBy.namespace]: (pod: Pod) => pod.getNs(), [sortBy.namespace]: (pod: Pod) => pod.getNs(),
@ -94,7 +96,7 @@ export class Pods extends React.Component<Props> {
renderHeaderTitle="Pods" renderHeaderTitle="Pods"
renderTableHeader={[ renderTableHeader={[
{ title: "Name", className: "name", sortBy: sortBy.name }, { title: "Name", className: "name", sortBy: sortBy.name },
{ className: "warning" }, { className: "warning", showWithColumn: "name" },
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, { title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
{ title: "Containers", className: "containers", sortBy: sortBy.containers }, { title: "Containers", className: "containers", sortBy: sortBy.containers },
{ title: "Restarts", className: "restarts", sortBy: sortBy.restarts }, { title: "Restarts", className: "restarts", sortBy: sortBy.restarts },

View File

@ -1,4 +1,5 @@
import "./item-list-layout.scss"; import "./item-list-layout.scss";
import "./table-menu.scss";
import groupBy from "lodash/groupBy"; import groupBy from "lodash/groupBy";
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
@ -18,6 +19,11 @@ import { PageFiltersList } from "./page-filters-list";
import { PageFiltersSelect } from "./page-filters-select"; import { PageFiltersSelect } from "./page-filters-select";
import { NamespaceSelectFilter } from "../+namespaces/namespace-select"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select";
import { themeStore } from "../../theme.store"; import { themeStore } from "../../theme.store";
import { MenuActions} from "../menu/menu-actions";
import { MenuItem } from "../menu";
import { Checkbox } from "../checkbox";
import { userStore } from "../../../common/user-store";
import logger from "../../../main/logger";
// todo: refactor, split to small re-usable components // todo: refactor, split to small re-usable components
@ -32,6 +38,7 @@ interface IHeaderPlaceholders {
} }
export interface ItemListLayoutProps<T extends ItemObject = ItemObject> { export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
tableId?: string;
className: IClassName; className: IClassName;
store: ItemStore<T>; store: ItemStore<T>;
dependentStores?: ItemStore[]; dependentStores?: ItemStore[];
@ -50,6 +57,7 @@ export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
isReady?: boolean; // show loading indicator while not ready isReady?: boolean; // show loading indicator while not ready
isSelectable?: boolean; // show checkbox in rows for selecting items isSelectable?: boolean; // show checkbox in rows for selecting items
isSearchable?: boolean; // apply search-filter & add search-input isSearchable?: boolean; // apply search-filter & add search-input
isConfigurable?: boolean;
copyClassNameFromHeadCells?: boolean; copyClassNameFromHeadCells?: boolean;
sortingCallbacks?: { [sortBy: string]: TableSortCallback }; sortingCallbacks?: { [sortBy: string]: TableSortCallback };
tableProps?: Partial<TableProps>; // low-level table configuration tableProps?: Partial<TableProps>; // low-level table configuration
@ -74,6 +82,7 @@ const defaultProps: Partial<ItemListLayoutProps> = {
showHeader: true, showHeader: true,
isSearchable: true, isSearchable: true,
isSelectable: true, isSelectable: true,
isConfigurable: false,
copyClassNameFromHeadCells: true, copyClassNameFromHeadCells: true,
dependentStores: [], dependentStores: [],
filterItems: [], filterItems: [],
@ -89,7 +98,7 @@ interface ItemListLayoutUserSettings {
@observer @observer
export class ItemListLayout extends React.Component<ItemListLayoutProps> { export class ItemListLayout extends React.Component<ItemListLayoutProps> {
static defaultProps = defaultProps as object; static defaultProps = defaultProps as object;
@observable hiddenColumnNames = new Set<string>();
@observable isUnmounting = false; @observable isUnmounting = false;
// default user settings (ui show-hide tweaks mostly) // default user settings (ui show-hide tweaks mostly)
@ -111,7 +120,10 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
} }
async componentDidMount() { async componentDidMount() {
const { store, dependentStores, isClusterScoped } = this.props; const { store, dependentStores, isClusterScoped, tableId } = this.props;
if (this.canBeConfigured) this.hiddenColumnNames = new Set(userStore.preferences?.hiddenTableColumns?.[tableId]);
const stores = [store, ...dependentStores]; const stores = [store, ...dependentStores];
if (!isClusterScoped) stores.push(namespaceStore); if (!isClusterScoped) stores.push(namespaceStore);
@ -216,6 +228,42 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
return this.applyFilters(filterItems, allItems); return this.applyFilters(filterItems, allItems);
} }
updateColumnFilter(checkboxValue: boolean, columnName: string) {
if (checkboxValue){
this.hiddenColumnNames.delete(columnName);
} else {
this.hiddenColumnNames.add(columnName);
}
if (this.canBeConfigured) {
userStore.preferences.hiddenTableColumns[this.props.tableId] = Array.from(this.hiddenColumnNames);
}
}
columnIsVisible(index: number): boolean {
const {renderTableHeader} = this.props;
if (!this.canBeConfigured) return true;
return !this.hiddenColumnNames.has(renderTableHeader[index].showWithColumn ?? renderTableHeader[index].className);
}
get canBeConfigured(): boolean {
const { isConfigurable, tableId, renderTableHeader } = this.props;
if (!isConfigurable || !tableId) {
return false;
}
if (!renderTableHeader?.every(({ className }) => className)) {
logger.warning("[ItemObjectList]: cannot configure an object list without all headers being identifiable");
return false;
}
return true;
}
@autobind() @autobind()
getRow(uid: string) { getRow(uid: string) {
const { const {
@ -259,7 +307,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
} }
} }
return <TableCell key={index} {...cellProps} />; return this.columnIsVisible(index) ? <TableCell key={index} {...cellProps} /> : null;
}) })
} }
{renderItemMenu && ( {renderItemMenu && (
@ -431,14 +479,19 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
onClick={prevDefault(() => store.toggleSelectionAll(items))} onClick={prevDefault(() => store.toggleSelectionAll(items))}
/> />
)} )}
{renderTableHeader.map((cellProps, index) => <TableCell key={index} {...cellProps} />)} {renderTableHeader.map((cellProps, index) => this.columnIsVisible(index) ? <TableCell key={index} {...cellProps} /> : null)}
{renderItemMenu && <TableCell className="menu" />} { renderItemMenu &&
<TableCell className="menu" >
{this.canBeConfigured && this.renderColumnMenu()}
</TableCell>
}
</TableHead> </TableHead>
)} )}
{ {
!virtual && items.map(item => this.getRow(item.getId())) !virtual && items.map(item => this.getRow(item.getId()))
} }
</Table> </Table>
)} )}
<AddRemoveButtons <AddRemoveButtons
onRemove={selectedItems.length ? removeItemsDialog : null} onRemove={selectedItems.length ? removeItemsDialog : null}
@ -449,6 +502,29 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
); );
} }
renderColumnMenu() {
const { renderTableHeader} = this.props;
return (
<MenuActions
toolbar = {false}
autoCloseOnSelect = {false}
className={cssNames("KubeObjectMenu")}
>
{renderTableHeader.map((cellProps, index) => (
!cellProps.showWithColumn &&
<MenuItem key={index} className="input">
<Checkbox label = {cellProps.title ?? `<${cellProps.className}>`}
className = "MenuCheckbox"
value ={!this.hiddenColumnNames.has(cellProps.className)}
onChange = {(v) => this.updateColumnFilter(v, cellProps.className)}
/>
</MenuItem>
))}
</MenuActions>
);
}
renderFooter() { renderFooter() {
if (this.props.renderFooter) { if (this.props.renderFooter) {
return this.props.renderFooter(this); return this.props.renderFooter(this);

View File

@ -0,0 +1,4 @@
.MenuCheckbox {
width: 100%;
height: 100%;
}

View File

@ -13,6 +13,7 @@ import isString from "lodash/isString";
export interface MenuActionsProps extends Partial<MenuProps> { export interface MenuActionsProps extends Partial<MenuProps> {
className?: string; className?: string;
toolbar?: boolean; // display menu as toolbar with icons toolbar?: boolean; // display menu as toolbar with icons
autoCloseOnSelect?: boolean;
triggerIcon?: string | IconProps | React.ReactNode; triggerIcon?: string | IconProps | React.ReactNode;
removeConfirmationMessage?: React.ReactNode | (() => React.ReactNode); removeConfirmationMessage?: React.ReactNode | (() => React.ReactNode);
updateAction?(): void; updateAction?(): void;
@ -80,7 +81,7 @@ export class MenuActions extends React.Component<MenuActionsProps> {
render() { render() {
const { const {
className, toolbar, children, updateAction, removeAction, triggerIcon, removeConfirmationMessage, className, toolbar, autoCloseOnSelect, children, updateAction, removeAction, triggerIcon, removeConfirmationMessage,
...menuProps ...menuProps
} = this.props; } = this.props;
const menuClassName = cssNames("MenuActions flex", className, { const menuClassName = cssNames("MenuActions flex", className, {
@ -98,7 +99,7 @@ export class MenuActions extends React.Component<MenuActionsProps> {
className={menuClassName} className={menuClassName}
usePortal={autoClose} usePortal={autoClose}
closeOnScroll={autoClose} closeOnScroll={autoClose}
closeOnClickItem={autoClose} closeOnClickItem={autoCloseOnSelect ?? autoClose }
closeOnClickOutside={autoClose} closeOnClickOutside={autoClose}
{...menuProps} {...menuProps}
> >

View File

@ -15,6 +15,7 @@ export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
isChecked?: boolean; // mark checkbox as checked or not isChecked?: boolean; // mark checkbox as checked or not
renderBoolean?: boolean; // show "true" or "false" for all of the children elements are "typeof boolean" renderBoolean?: boolean; // show "true" or "false" for all of the children elements are "typeof boolean"
sortBy?: TableSortBy; // column name, must be same as key in sortable object <Table sortable={}/> sortBy?: TableSortBy; // column name, must be same as key in sortable object <Table sortable={}/>
showWithColumn?: string // className of another column, if it is not empty the current column is not shown in the filter menu, visibility of this one is the same as a specified column, applicable to headers only
_sorting?: Partial<TableSortParams>; // <Table> sorting state, don't use this prop outside (!) _sorting?: Partial<TableSortParams>; // <Table> sorting state, don't use this prop outside (!)
_sort?(sortBy: TableSortBy): void; // <Table> sort function, don't use this prop outside (!) _sort?(sortBy: TableSortBy): void; // <Table> sort function, don't use this prop outside (!)
_nowrap?: boolean; // indicator, might come from parent <TableHead>, don't use this prop outside (!) _nowrap?: boolean; // indicator, might come from parent <TableHead>, don't use this prop outside (!)
@ -63,7 +64,7 @@ export class TableCell extends React.Component<TableCellProps> {
} }
render() { render() {
const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, renderBoolean: displayBoolean, ...cellProps } = this.props; const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, renderBoolean: displayBoolean, showWithColumn, ...cellProps } = this.props;
const classNames = cssNames("TableCell", className, { const classNames = cssNames("TableCell", className, {
checkbox, checkbox,
nowrap: _nowrap, nowrap: _nowrap,