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

[WIP] Resizable columns

Signed-off-by: Pavel Ashevskii <pashevskii@mirantis.com>
This commit is contained in:
Pavel Ashevskii 2020-12-18 17:39:16 +04:00
parent 1e5d682b9b
commit 2922b7ae96
7 changed files with 168 additions and 56 deletions

View File

@ -28,7 +28,8 @@ export interface UserPreferences {
downloadBinariesPath?: string; downloadBinariesPath?: string;
kubectlBinariesPath?: string; kubectlBinariesPath?: string;
openAtLogin?: boolean; openAtLogin?: boolean;
hiddenTableColumns?: Record<string, string[]>
storeTableConfig?: Record<string, Record<string, {width?:number, visible?:boolean, index?: number}>>;
} }
export class UserStore extends BaseStore<UserStoreModel> { export class UserStore extends BaseStore<UserStoreModel> {
@ -55,7 +56,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: {}, storeTableConfig: {},
}; };
protected async handleOnLoad() { protected async handleOnLoad() {
@ -84,15 +85,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
return semver.gt(getAppVersion(), this.lastSeenAppVersion); return semver.gt(getAppVersion(), this.lastSeenAppVersion);
} }
@action
setHiddenTableColumns(tableId: string, names: Set<string> | string[]) {
this.preferences.hiddenTableColumns[tableId] = Array.from(names);
}
getHiddenTableColumns(tableId: string): Set<string> {
return new Set(this.preferences.hiddenTableColumns[tableId]);
}
@action @action
resetKubeConfigPath() { resetKubeConfigPath() {
this.kubeConfigPath = kubeConfigDefaultPath; this.kubeConfigPath = kubeConfigDefaultPath;

View File

@ -71,10 +71,11 @@ export class Pods extends React.Component<Props> {
render() { render() {
return ( return (
<KubeObjectListLayout <KubeObjectListLayout
tableId = "workloads_pods"
isResizable
isConfigurable
className="Pods" store={podsStore} className="Pods" store={podsStore}
dependentStores={[volumeClaimStore, eventStore]} dependentStores={[volumeClaimStore, eventStore]}
tableId = "workloads_pods"
isConfigurable
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: (pod: Pod) => pod.getName(), [columnId.name]: (pod: Pod) => pod.getName(),
[columnId.namespace]: (pod: Pod) => pod.getNs(), [columnId.namespace]: (pod: Pod) => pod.getNs(),

View File

@ -1,6 +1,8 @@
.ItemListLayout { .ItemListLayout {
background: $contentColor; background: $contentColor;
height: 100%; height: 100%;
min-width: 100%;
width: fit-content;
> .header { > .header {
--flex-gap: #{$padding * 2}; --flex-gap: #{$padding * 2};

View File

@ -20,8 +20,11 @@ import { themeStore } from "../../theme.store";
import { MenuActions } from "../menu/menu-actions"; import { MenuActions } from "../menu/menu-actions";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
import { Checkbox } from "../checkbox"; import { Checkbox } from "../checkbox";
import { Button } from "../button";
import { userStore } from "../../../common/user-store"; import { userStore } from "../../../common/user-store";
import { namespaceStore } from "../+namespaces/namespace.store"; import { namespaceStore } from "../+namespaces/namespace.store";
import logger from "../../../main/logger";
// todo: refactor, split to small re-usable components // todo: refactor, split to small re-usable components
@ -58,6 +61,7 @@ export interface ItemListLayoutProps<T extends ItemObject = ItemObject> {
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; isConfigurable?: boolean;
isResizable?: 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
@ -83,6 +87,7 @@ const defaultProps: Partial<ItemListLayoutProps> = {
isSearchable: true, isSearchable: true,
isSelectable: true, isSelectable: true,
isConfigurable: false, isConfigurable: false,
isResizable: false,
copyClassNameFromHeadCells: true, copyClassNameFromHeadCells: true,
preloadStores: true, preloadStores: true,
dependentStores: [], dependentStores: [],
@ -99,7 +104,12 @@ 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;
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<string,number>();
@observable userSettings: ItemListLayoutUserSettings = { @observable userSettings: ItemListLayoutUserSettings = {
showAppliedFilters: false, showAppliedFilters: false,
}; };
@ -115,14 +125,14 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction(() => toJS(this.userSettings), settings => storage.set(settings)), reaction(() => toJS(this.userSettings), settings => storage.set(settings)),
]); ]);
if (this.canBeResized || this.canBeConfigured) {
this.initColumnConfig();
}
} }
async componentDidMount() { async componentDidMount() {
const { isClusterScoped, isConfigurable, tableId, preloadStores } = this.props; const { isClusterScoped, preloadStores } = this.props;
if (isConfigurable && !tableId) {
throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified");
}
if (preloadStores) { if (preloadStores) {
this.loadStores(); this.loadStores();
@ -133,6 +143,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
]); ]);
} }
} }
} }
private loadStores() { private loadStores() {
@ -143,6 +154,68 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
stores.forEach(store => store.loadAll(namespaceStore.contextNamespaces)); 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 } = { private filterCallbacks: { [type: string]: ItemsFilter } = {
[FilterType.SEARCH]: items => { [FilterType.SEARCH]: items => {
const { searchFilters, isSearchable } = this.props; const { searchFilters, isSearchable } = this.props;
@ -220,6 +293,31 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
return this.applyFilters(filterItems, items); 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() @autobind()
getRow(uid: string) { getRow(uid: string) {
const { const {
@ -260,7 +358,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
} }
if (!headCell || !this.isHiddenColumn(headCell)) { if (!headCell || !this.isHiddenColumn(headCell)) {
return <TableCell key={index} {...cellProps} />; return <TableCell key={index} {...cellProps} style={this.getStylesOfResizedColumn(cellProps.className)}/>;
} }
}) })
} }
@ -362,7 +460,13 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
title: <h5 className="title">{title}</h5>, title: <h5 className="title">{title}</h5>,
info: this.renderInfo(), info: this.renderInfo(),
filters: <> filters: <>
{!isClusterScoped && <NamespaceSelectFilter/>} <Button primary
hidden={!this.showResetColumnsWidthsButton}
onClick={()=>{
this.props.renderTableHeader.map((cellProps) => this.getColumnConfig(cellProps.className).width = null);
this.showResetColumnsWidthsButton = false;
}}>Reset</Button>
{!isClusterScoped && <NamespaceSelectFilter />}
<PageFiltersSelect allowEmpty disableFilters={{ <PageFiltersSelect allowEmpty disableFilters={{
[FilterType.NAMESPACE]: true, // namespace-select used instead [FilterType.NAMESPACE]: true, // namespace-select used instead
}}/> }}/>
@ -392,7 +496,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
} }
renderTableHeader() { renderTableHeader() {
const { renderTableHeader, isSelectable, isConfigurable, store } = this.props; const { renderTableHeader, isSelectable, isResizable, store } = this.props;
if (!renderTableHeader) { if (!renderTableHeader) {
return; return;
@ -407,13 +511,13 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
onClick={prevDefault(() => store.toggleSelectionAll(this.items))} onClick={prevDefault(() => store.toggleSelectionAll(this.items))}
/> />
)} )}
{renderTableHeader.map((cellProps, index) => { {renderTableHeader.map((cellProps, index) => !this.isHiddenColumn(cellProps) && <TableCell isResizable={isResizable} key={index} {...cellProps}
if (!this.isHiddenColumn(cellProps)) { getCurrentExtent = {() => this.getColumnCalculatedWidth(cellProps.className)}
return <TableCell key={cellProps.id ?? index} {...cellProps} />; onResize = {v => this.resizeColumn(cellProps.className,v)}
} onResizeStop = {() => this.saveColumnsConfig()}
})} style={this.getStylesOfResizedColumn(cellProps.className)}/>)}
<TableCell className="menu"> <TableCell className="menu" >
{isConfigurable && this.renderColumnVisibilityMenu()} {this.canBeConfigured && this.renderColumnVisibilityMenu()}
</TableCell> </TableCell>
</TableHead> </TableHead>
); );
@ -421,8 +525,8 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
renderList() { renderList() {
const { const {
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem, tableProps = {}, store, hasDetailsView,
tableProps = {}, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem
} = this.props; } = this.props;
const { isReady, removeItemsDialog, items } = this; const { isReady, removeItemsDialog, items } = this;
const { selectedItems } = store; const { selectedItems } = store;
@ -463,30 +567,16 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
); );
} }
@computed get hiddenColumns() { isHiddenColumn({ className: columnName, showWithColumn }: TableCellProps): boolean {
return userStore.getHiddenTableColumns(this.props.tableId); if (!this.canBeConfigured) {
}
isHiddenColumn({ id: columnId, showWithColumn }: TableCellProps): boolean {
if (!this.props.isConfigurable) {
return false; return false;
} }
return this.hiddenColumns.has(columnId) || ( return !this.getColumnConfig(showWithColumn ?? columnName)?.visible ?? false;
showWithColumn && this.hiddenColumns.has(showWithColumn)
);
} }
updateColumnVisibility({ id: columnId }: TableCellProps, isVisible: boolean) { updateColumnVisibility({ className: columnName }: TableCellProps, isVisible: boolean) {
const hiddenColumns = new Set(this.hiddenColumns); this.getColumnConfig(columnName).visible = isVisible;
if (!isVisible) {
hiddenColumns.add(columnId);
} else {
hiddenColumns.delete(columnId);
}
userStore.setHiddenTableColumns(this.props.tableId, hiddenColumns);
} }
renderColumnVisibilityMenu() { renderColumnVisibilityMenu() {
@ -498,8 +588,8 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
!cellProps.showWithColumn && ( !cellProps.showWithColumn && (
<MenuItem key={index} className="input"> <MenuItem key={index} className="input">
<Checkbox <Checkbox
label={cellProps.title ?? `<${cellProps.className}>`} label = {cellProps.title ?? `<${cellProps.className}>`}
value={!this.isHiddenColumn(cellProps)} value ={!this.isHiddenColumn(cellProps)}
onChange={isVisible => this.updateColumnVisibility(cellProps, isVisible)} onChange={isVisible => this.updateColumnVisibility(cellProps, isVisible)}
/> />
</MenuItem> </MenuItem>

View File

@ -3,6 +3,7 @@
word-break: break-all; word-break: break-all;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
position: relative;
&.checkbox { &.checkbox {
display: flex; display: flex;
@ -66,4 +67,4 @@
color: $textColorAccent; color: $textColorAccent;
} }
} }
} }

View File

@ -1,10 +1,11 @@
import "./table-cell.scss"; import "./table-cell.scss";
import type { TableSortBy, TableSortParams } from "./table"; import type { TableSortBy, TableSortParams } from "./table";
import React, { ReactNode } from "react"; import React, { ReactNode, CSSProperties } from "react";
import { autobind, cssNames, displayBooleans } from "../../utils"; import { autobind, cssNames, displayBooleans } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Checkbox } from "../checkbox"; import { Checkbox } from "../checkbox";
import { ResizeDirection, ResizeSide, ResizeGrowthDirection, ResizingAnchor } from "../resizing-anchor";
export type TableCellElem = React.ReactElement<TableCellProps>; export type TableCellElem = React.ReactElement<TableCellProps>;
@ -16,7 +17,12 @@ 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 // 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<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 (!)
@ -64,8 +70,27 @@ export class TableCell extends React.Component<TableCellProps> {
} }
} }
renderResizingAnchor() {
const { onResize, onResizeStop, isResizable, getCurrentExtent } = this.props;
if(isResizable) {
return (
<ResizingAnchor
direction={ResizeDirection.HORIZONTAL}
placement={ResizeSide.TRAILING}
growthDirection={ResizeGrowthDirection.LEFT_TO_RIGHT}
getCurrentExtent={getCurrentExtent}
onDrag={onResize}
onEnd={onResizeStop}
minExtent={30}
maxExtent={900}
/>
);
}
}
render() { 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, { const classNames = cssNames("TableCell", className, {
checkbox, checkbox,
nowrap: _nowrap, nowrap: _nowrap,
@ -78,6 +103,7 @@ export class TableCell extends React.Component<TableCellProps> {
{this.renderCheckbox()} {this.renderCheckbox()}
{_nowrap ? <div className="content">{content}</div> : content} {_nowrap ? <div className="content">{content}</div> : content}
{this.renderSortIcon()} {this.renderSortIcon()}
{this.renderResizingAnchor()}
</div> </div>
); );
} }

View File

@ -34,4 +34,4 @@
} }
} }
} }
} }