diff --git a/src/common/user-store.ts b/src/common/user-store.ts index b0294d9e5a..9370abb443 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -28,7 +28,8 @@ export interface UserPreferences { downloadBinariesPath?: string; kubectlBinariesPath?: string; openAtLogin?: boolean; - hiddenTableColumns?: Record + + storeTableConfig?: Record>; } export class UserStore extends BaseStore { @@ -55,7 +56,7 @@ export class UserStore extends BaseStore { downloadMirror: "default", downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version openAtLogin: false, - hiddenTableColumns: {}, + storeTableConfig: {}, }; protected async handleOnLoad() { @@ -84,15 +85,6 @@ export class UserStore extends BaseStore { return semver.gt(getAppVersion(), this.lastSeenAppVersion); } - @action - setHiddenTableColumns(tableId: string, names: Set | string[]) { - this.preferences.hiddenTableColumns[tableId] = Array.from(names); - } - - getHiddenTableColumns(tableId: string): Set { - return new Set(this.preferences.hiddenTableColumns[tableId]); - } - @action resetKubeConfigPath() { this.kubeConfigPath = kubeConfigDefaultPath; diff --git a/src/renderer/components/+workloads-pods/pods.tsx b/src/renderer/components/+workloads-pods/pods.tsx index a59c9d79d2..87f5dd5907 100644 --- a/src/renderer/components/+workloads-pods/pods.tsx +++ b/src/renderer/components/+workloads-pods/pods.tsx @@ -71,10 +71,11 @@ export class Pods extends React.Component { render() { return ( pod.getName(), [columnId.namespace]: (pod: Pod) => pod.getNs(), diff --git a/src/renderer/components/item-object-list/item-list-layout.scss b/src/renderer/components/item-object-list/item-list-layout.scss index 0008ffd527..d908440c9d 100644 --- a/src/renderer/components/item-object-list/item-list-layout.scss +++ b/src/renderer/components/item-object-list/item-list-layout.scss @@ -1,6 +1,8 @@ .ItemListLayout { background: $contentColor; height: 100%; + min-width: 100%; + width: fit-content; > .header { --flex-gap: #{$padding * 2}; 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 59bd18cd5e..671c5491a8 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -20,8 +20,11 @@ import { themeStore } from "../../theme.store"; import { MenuActions } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Checkbox } from "../checkbox"; +import { Button } from "../button"; import { userStore } from "../../../common/user-store"; import { namespaceStore } from "../+namespaces/namespace.store"; +import logger from "../../../main/logger"; + // todo: refactor, split to small re-usable components @@ -58,6 +61,7 @@ export interface ItemListLayoutProps { isSelectable?: boolean; // show checkbox in rows for selecting items isSearchable?: boolean; // apply search-filter & add search-input isConfigurable?: boolean; + isResizable?: boolean; copyClassNameFromHeadCells?: boolean; sortingCallbacks?: { [sortBy: string]: TableSortCallback }; tableProps?: Partial; // low-level table configuration @@ -83,6 +87,7 @@ const defaultProps: Partial = { isSearchable: true, isSelectable: true, isConfigurable: false, + isResizable: false, copyClassNameFromHeadCells: true, preloadStores: true, dependentStores: [], @@ -99,7 +104,12 @@ interface ItemListLayoutUserSettings { @observer export class ItemListLayout extends React.Component { static defaultProps = defaultProps as object; + private resetTableButtonTimer:any = null; + @observable showResetColumnsWidthsButton = false; + // columnWidthsTmp is for temporary storing column widths at moment between the start and stop of resizing + // it helps to avoid excess writing to the file system + @observable columnWidthsTmp = new Map(); @observable userSettings: ItemListLayoutUserSettings = { showAppliedFilters: false, }; @@ -115,14 +125,14 @@ export class ItemListLayout extends React.Component { disposeOnUnmount(this, [ reaction(() => toJS(this.userSettings), settings => storage.set(settings)), ]); + + if (this.canBeResized || this.canBeConfigured) { + this.initColumnConfig(); + } } async componentDidMount() { - const { isClusterScoped, isConfigurable, tableId, preloadStores } = this.props; - - if (isConfigurable && !tableId) { - throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified"); - } + const { isClusterScoped, preloadStores } = this.props; if (preloadStores) { this.loadStores(); @@ -133,6 +143,7 @@ export class ItemListLayout extends React.Component { ]); } } + } private loadStores() { @@ -143,6 +154,68 @@ export class ItemListLayout extends React.Component { stores.forEach(store => store.loadAll(namespaceStore.contextNamespaces)); } + initColumnConfig() { + const {renderTableHeader, tableId } = this.props; + + if ( !userStore.preferences?.storeTableConfig?.[tableId] ) { + userStore.preferences.storeTableConfig[tableId] = {}; + renderTableHeader.map((cellProps, index) => { + userStore.preferences.storeTableConfig[tableId][cellProps.className] = {index, visible: true}; + }); + } + } + + + saveColumnsConfig() { + this.columnWidthsTmp.forEach((value, key) => { + this.getColumnConfig(key).width = value; + }); + this.columnWidthsTmp.clear(); + } + + + getColumnConfig(column: string) { + return userStore.preferences.storeTableConfig[this.props.tableId]?.[column]; + } + + + getColumnCalculatedWidth(className: string) : number { + return document.querySelector(`.TableHead > .${className}`).clientWidth; + } + + + resizeColumn(className: string, width: number){ + const { renderTableHeader } = this.props; + + this.showResetColumnsWidthsButton = true; + + for (let _i = 0; _i < this.getColumnConfig(className).index; _i++) { + if( this.getColumnConfig(renderTableHeader[_i].showWithColumn ?? renderTableHeader[_i].className)?.visible + && !this.getColumnConfig(renderTableHeader[_i].showWithColumn ?? renderTableHeader[_i].className)?.width) { + const componentWidth = this.getColumnCalculatedWidth(renderTableHeader[_i].className); + + this.columnWidthsTmp.set(renderTableHeader[_i].className, componentWidth); + } + } + this.columnWidthsTmp.set(className, width); + + //Clear existing timer and set new one + clearTimeout(this.resetTableButtonTimer); + this.resetTableButtonTimer = setTimeout(() => this.showResetColumnsWidthsButton = false, 10000); + } + + getStylesOfResizedColumn(className: string) :any { + if (this.columnWidthsTmp.get(className) || this.getColumnConfig(className)?.width) { + return ( + { + //"flex-basis": `${this.columnsConfig[className].width}px`, + "min-width": `${this.columnWidthsTmp.get(className)?? this.getColumnConfig(className).width}px`, + "flex-grow": "0", + "flex-shrink": "0"} + ); + } + } + private filterCallbacks: { [type: string]: ItemsFilter } = { [FilterType.SEARCH]: items => { const { searchFilters, isSearchable } = this.props; @@ -220,6 +293,31 @@ export class ItemListLayout extends React.Component { return this.applyFilters(filterItems, items); } + get canBeConfigured(): boolean { + return this.props.isConfigurable && this.canBeChanged; + } + + get canBeResized(): boolean { + return this.props.isResizable && this.canBeChanged; + } + + get canBeChanged(): boolean { + const { tableId, renderTableHeader } = this.props; + + if (!tableId) { + return false; + } + + if (!renderTableHeader?.every(({ className }) => className)) { + logger.warning("[ItemObjectList]: cannot change an object list without all headers being identifiable"); + + return false; + } + + return true; + } + + @autobind() getRow(uid: string) { const { @@ -260,7 +358,7 @@ export class ItemListLayout extends React.Component { } if (!headCell || !this.isHiddenColumn(headCell)) { - return ; + return ; } }) } @@ -362,7 +460,13 @@ export class ItemListLayout extends React.Component { title:
{title}
, info: this.renderInfo(), filters: <> - {!isClusterScoped && } + + {!isClusterScoped && } @@ -392,7 +496,7 @@ export class ItemListLayout extends React.Component { } renderTableHeader() { - const { renderTableHeader, isSelectable, isConfigurable, store } = this.props; + const { renderTableHeader, isSelectable, isResizable, store } = this.props; if (!renderTableHeader) { return; @@ -407,13 +511,13 @@ export class ItemListLayout extends React.Component { onClick={prevDefault(() => store.toggleSelectionAll(this.items))} /> )} - {renderTableHeader.map((cellProps, index) => { - if (!this.isHiddenColumn(cellProps)) { - return ; - } - })} - - {isConfigurable && this.renderColumnVisibilityMenu()} + {renderTableHeader.map((cellProps, index) => !this.isHiddenColumn(cellProps) && this.getColumnCalculatedWidth(cellProps.className)} + onResize = {v => this.resizeColumn(cellProps.className,v)} + onResizeStop = {() => this.saveColumnsConfig()} + style={this.getStylesOfResizedColumn(cellProps.className)}/>)} + + {this.canBeConfigured && this.renderColumnVisibilityMenu()} ); @@ -421,8 +525,8 @@ export class ItemListLayout extends React.Component { renderList() { const { - store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem, - tableProps = {}, + tableProps = {}, store, hasDetailsView, + addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem } = this.props; const { isReady, removeItemsDialog, items } = this; const { selectedItems } = store; @@ -463,30 +567,16 @@ export class ItemListLayout extends React.Component { ); } - @computed get hiddenColumns() { - return userStore.getHiddenTableColumns(this.props.tableId); - } - - isHiddenColumn({ id: columnId, showWithColumn }: TableCellProps): boolean { - if (!this.props.isConfigurable) { + isHiddenColumn({ className: columnName, showWithColumn }: TableCellProps): boolean { + if (!this.canBeConfigured) { return false; } - return this.hiddenColumns.has(columnId) || ( - showWithColumn && this.hiddenColumns.has(showWithColumn) - ); + return !this.getColumnConfig(showWithColumn ?? columnName)?.visible ?? false; } - updateColumnVisibility({ id: columnId }: TableCellProps, isVisible: boolean) { - const hiddenColumns = new Set(this.hiddenColumns); - - if (!isVisible) { - hiddenColumns.add(columnId); - } else { - hiddenColumns.delete(columnId); - } - - userStore.setHiddenTableColumns(this.props.tableId, hiddenColumns); + updateColumnVisibility({ className: columnName }: TableCellProps, isVisible: boolean) { + this.getColumnConfig(columnName).visible = isVisible; } renderColumnVisibilityMenu() { @@ -498,8 +588,8 @@ export class ItemListLayout extends React.Component { !cellProps.showWithColumn && ( `} - value={!this.isHiddenColumn(cellProps)} + label = {cellProps.title ?? `<${cellProps.className}>`} + value ={!this.isHiddenColumn(cellProps)} onChange={isVisible => this.updateColumnVisibility(cellProps, isVisible)} /> diff --git a/src/renderer/components/table/table-cell.scss b/src/renderer/components/table/table-cell.scss index 5c12889c7e..983549100d 100644 --- a/src/renderer/components/table/table-cell.scss +++ b/src/renderer/components/table/table-cell.scss @@ -3,6 +3,7 @@ word-break: break-all; overflow: hidden; text-overflow: ellipsis; + position: relative; &.checkbox { display: flex; @@ -66,4 +67,4 @@ color: $textColorAccent; } } -} \ No newline at end of file +} diff --git a/src/renderer/components/table/table-cell.tsx b/src/renderer/components/table/table-cell.tsx index 81e2f9f85f..ad16cdc796 100644 --- a/src/renderer/components/table/table-cell.tsx +++ b/src/renderer/components/table/table-cell.tsx @@ -1,10 +1,11 @@ import "./table-cell.scss"; import type { TableSortBy, TableSortParams } from "./table"; -import React, { ReactNode } from "react"; +import React, { ReactNode, CSSProperties } from "react"; import { autobind, cssNames, displayBooleans } from "../../utils"; import { Icon } from "../icon"; import { Checkbox } from "../checkbox"; +import { ResizeDirection, ResizeSide, ResizeGrowthDirection, ResizingAnchor } from "../resizing-anchor"; export type TableCellElem = React.ReactElement; @@ -16,7 +17,12 @@ 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 // id of the column which follow same visibility rules + showWithColumn?: string + style?: CSSProperties; + isResizable?: boolean; + getCurrentExtent?: () => number; + onResize?: (newExtent: number) => void; + onResizeStop?: () => void; _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 (!) @@ -64,8 +70,27 @@ export class TableCell extends React.Component { } } + renderResizingAnchor() { + const { onResize, onResizeStop, isResizable, getCurrentExtent } = this.props; + + if(isResizable) { + return ( + + ); + } + } + render() { - const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, renderBoolean: displayBoolean, showWithColumn, ...cellProps } = this.props; + const { onResize, onResizeStop, className, isResizable, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, renderBoolean: displayBoolean, showWithColumn, ...cellProps } = this.props; const classNames = cssNames("TableCell", className, { checkbox, nowrap: _nowrap, @@ -78,6 +103,7 @@ export class TableCell extends React.Component { {this.renderCheckbox()} {_nowrap ?
{content}
: content} {this.renderSortIcon()} + {this.renderResizingAnchor()} ); } diff --git a/src/renderer/components/table/table-head.scss b/src/renderer/components/table/table-head.scss index 6b4ba77df0..34f2678d8b 100644 --- a/src/renderer/components/table/table-head.scss +++ b/src/renderer/components/table/table-head.scss @@ -34,4 +34,4 @@ } } } -} \ No newline at end of file +}