diff --git a/src/common/user-store.ts b/src/common/user-store.ts index 1e9f7646f3..cf271a011d 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -28,6 +28,7 @@ export interface UserPreferences { downloadBinariesPath?: string; kubectlBinariesPath?: string; openAtLogin?: boolean; + hiddenTableColumns?: Record } export class UserStore extends BaseStore { @@ -54,6 +55,7 @@ export class UserStore extends BaseStore { downloadMirror: "default", downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version openAtLogin: false, + hiddenTableColumns: {}, }; protected async handleOnLoad() { diff --git a/src/renderer/components/+workloads-pods/pods.tsx b/src/renderer/components/+workloads-pods/pods.tsx index 88981b968f..2296b98317 100644 --- a/src/renderer/components/+workloads-pods/pods.tsx +++ b/src/renderer/components/+workloads-pods/pods.tsx @@ -74,6 +74,8 @@ export class Pods extends React.Component { pod.getName(), [sortBy.namespace]: (pod: Pod) => pod.getNs(), @@ -94,7 +96,7 @@ export class Pods extends React.Component { renderHeaderTitle="Pods" renderTableHeader={[ { title: "Name", className: "name", sortBy: sortBy.name }, - { className: "warning" }, + { className: "warning", showWithColumn: "name" }, { title: "Namespace", className: "namespace", sortBy: sortBy.namespace }, { title: "Containers", className: "containers", sortBy: sortBy.containers }, { title: "Restarts", className: "restarts", sortBy: sortBy.restarts }, 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 bdd6302f5a..38e0e0218d 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -1,4 +1,5 @@ import "./item-list-layout.scss"; +import "./table-menu.scss"; import groupBy from "lodash/groupBy"; import React, { ReactNode } from "react"; @@ -18,6 +19,11 @@ import { PageFiltersList } from "./page-filters-list"; import { PageFiltersSelect } from "./page-filters-select"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select"; 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 @@ -32,6 +38,7 @@ interface IHeaderPlaceholders { } export interface ItemListLayoutProps { + tableId?: string; className: IClassName; store: ItemStore; dependentStores?: ItemStore[]; @@ -50,6 +57,7 @@ export interface ItemListLayoutProps { 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 }; tableProps?: Partial; // low-level table configuration @@ -74,6 +82,7 @@ const defaultProps: Partial = { showHeader: true, isSearchable: true, isSelectable: true, + isConfigurable: false, copyClassNameFromHeadCells: true, dependentStores: [], filterItems: [], @@ -89,7 +98,7 @@ interface ItemListLayoutUserSettings { @observer export class ItemListLayout extends React.Component { static defaultProps = defaultProps as object; - + @observable hiddenColumnNames = new Set(); @observable isUnmounting = false; // default user settings (ui show-hide tweaks mostly) @@ -111,7 +120,10 @@ export class ItemListLayout extends React.Component { } 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]; if (!isClusterScoped) stores.push(namespaceStore); @@ -216,6 +228,42 @@ export class ItemListLayout extends React.Component { 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() getRow(uid: string) { const { @@ -259,7 +307,7 @@ export class ItemListLayout extends React.Component { } } - return ; + return this.columnIsVisible(index) ? : null; }) } {renderItemMenu && ( @@ -431,14 +479,19 @@ export class ItemListLayout extends React.Component { onClick={prevDefault(() => store.toggleSelectionAll(items))} /> )} - {renderTableHeader.map((cellProps, index) => )} - {renderItemMenu && } + {renderTableHeader.map((cellProps, index) => this.columnIsVisible(index) ? : null)} + { renderItemMenu && + + {this.canBeConfigured && this.renderColumnMenu()} + + } )} { !virtual && items.map(item => this.getRow(item.getId())) } + )} { ); } + renderColumnMenu() { + const { renderTableHeader} = this.props; + + return ( + + {renderTableHeader.map((cellProps, index) => ( + !cellProps.showWithColumn && + + `} + className = "MenuCheckbox" + value ={!this.hiddenColumnNames.has(cellProps.className)} + onChange = {(v) => this.updateColumnFilter(v, cellProps.className)} + /> + + ))} + + ); + } + renderFooter() { if (this.props.renderFooter) { return this.props.renderFooter(this); diff --git a/src/renderer/components/item-object-list/table-menu.scss b/src/renderer/components/item-object-list/table-menu.scss new file mode 100644 index 0000000000..b7e41f54ca --- /dev/null +++ b/src/renderer/components/item-object-list/table-menu.scss @@ -0,0 +1,4 @@ +.MenuCheckbox { + width: 100%; + height: 100%; +} diff --git a/src/renderer/components/menu/menu-actions.tsx b/src/renderer/components/menu/menu-actions.tsx index aa8191dd7f..4e46935aa4 100644 --- a/src/renderer/components/menu/menu-actions.tsx +++ b/src/renderer/components/menu/menu-actions.tsx @@ -13,6 +13,7 @@ import isString from "lodash/isString"; export interface MenuActionsProps extends Partial { className?: string; toolbar?: boolean; // display menu as toolbar with icons + autoCloseOnSelect?: boolean; triggerIcon?: string | IconProps | React.ReactNode; removeConfirmationMessage?: React.ReactNode | (() => React.ReactNode); updateAction?(): void; @@ -80,7 +81,7 @@ export class MenuActions extends React.Component { render() { const { - className, toolbar, children, updateAction, removeAction, triggerIcon, removeConfirmationMessage, + className, toolbar, autoCloseOnSelect, children, updateAction, removeAction, triggerIcon, removeConfirmationMessage, ...menuProps } = this.props; const menuClassName = cssNames("MenuActions flex", className, { @@ -98,7 +99,7 @@ export class MenuActions extends React.Component { className={menuClassName} usePortal={autoClose} closeOnScroll={autoClose} - closeOnClickItem={autoClose} + closeOnClickItem={autoCloseOnSelect ?? autoClose } closeOnClickOutside={autoClose} {...menuProps} > diff --git a/src/renderer/components/table/table-cell.tsx b/src/renderer/components/table/table-cell.tsx index a42db4c2be..97335078f1 100644 --- a/src/renderer/components/table/table-cell.tsx +++ b/src/renderer/components/table/table-cell.tsx @@ -15,6 +15,7 @@ export interface TableCellProps extends React.DOMAttributes { isChecked?: boolean; // mark checkbox as checked or not 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 + 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; //
sorting state, don't use this prop outside (!) _sort?(sortBy: TableSortBy): void; //
sort function, don't use this prop outside (!) _nowrap?: boolean; // indicator, might come from parent , don't use this prop outside (!) @@ -63,7 +64,7 @@ export class TableCell extends React.Component { } 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, { checkbox, nowrap: _nowrap,