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:
parent
1e5d682b9b
commit
2922b7ae96
@ -28,7 +28,8 @@ export interface UserPreferences {
|
||||
downloadBinariesPath?: string;
|
||||
kubectlBinariesPath?: string;
|
||||
openAtLogin?: boolean;
|
||||
hiddenTableColumns?: Record<string, string[]>
|
||||
|
||||
storeTableConfig?: Record<string, Record<string, {width?:number, visible?:boolean, index?: number}>>;
|
||||
}
|
||||
|
||||
export class UserStore extends BaseStore<UserStoreModel> {
|
||||
@ -55,7 +56,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
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<UserStoreModel> {
|
||||
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
|
||||
resetKubeConfigPath() {
|
||||
this.kubeConfigPath = kubeConfigDefaultPath;
|
||||
|
||||
@ -71,10 +71,11 @@ export class Pods extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<KubeObjectListLayout
|
||||
tableId = "workloads_pods"
|
||||
isResizable
|
||||
isConfigurable
|
||||
className="Pods" store={podsStore}
|
||||
dependentStores={[volumeClaimStore, eventStore]}
|
||||
tableId = "workloads_pods"
|
||||
isConfigurable
|
||||
sortingCallbacks={{
|
||||
[columnId.name]: (pod: Pod) => pod.getName(),
|
||||
[columnId.namespace]: (pod: Pod) => pod.getNs(),
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
.ItemListLayout {
|
||||
background: $contentColor;
|
||||
height: 100%;
|
||||
min-width: 100%;
|
||||
width: fit-content;
|
||||
|
||||
> .header {
|
||||
--flex-gap: #{$padding * 2};
|
||||
|
||||
@ -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<T extends ItemObject = ItemObject> {
|
||||
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<TableProps>; // low-level table configuration
|
||||
@ -83,6 +87,7 @@ const defaultProps: Partial<ItemListLayoutProps> = {
|
||||
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<ItemListLayoutProps> {
|
||||
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 = {
|
||||
showAppliedFilters: false,
|
||||
};
|
||||
@ -115,14 +125,14 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
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<ItemListLayoutProps> {
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private loadStores() {
|
||||
@ -143,6 +154,68 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
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<ItemListLayoutProps> {
|
||||
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<ItemListLayoutProps> {
|
||||
}
|
||||
|
||||
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>,
|
||||
info: this.renderInfo(),
|
||||
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={{
|
||||
[FilterType.NAMESPACE]: true, // namespace-select used instead
|
||||
}}/>
|
||||
@ -392,7 +496,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
}
|
||||
|
||||
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<ItemListLayoutProps> {
|
||||
onClick={prevDefault(() => store.toggleSelectionAll(this.items))}
|
||||
/>
|
||||
)}
|
||||
{renderTableHeader.map((cellProps, index) => {
|
||||
if (!this.isHiddenColumn(cellProps)) {
|
||||
return <TableCell key={cellProps.id ?? index} {...cellProps} />;
|
||||
}
|
||||
})}
|
||||
<TableCell className="menu">
|
||||
{isConfigurable && this.renderColumnVisibilityMenu()}
|
||||
{renderTableHeader.map((cellProps, index) => !this.isHiddenColumn(cellProps) && <TableCell isResizable={isResizable} key={index} {...cellProps}
|
||||
getCurrentExtent = {() => this.getColumnCalculatedWidth(cellProps.className)}
|
||||
onResize = {v => this.resizeColumn(cellProps.className,v)}
|
||||
onResizeStop = {() => this.saveColumnsConfig()}
|
||||
style={this.getStylesOfResizedColumn(cellProps.className)}/>)}
|
||||
<TableCell className="menu" >
|
||||
{this.canBeConfigured && this.renderColumnVisibilityMenu()}
|
||||
</TableCell>
|
||||
</TableHead>
|
||||
);
|
||||
@ -421,8 +525,8 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
|
||||
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<ItemListLayoutProps> {
|
||||
);
|
||||
}
|
||||
|
||||
@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<ItemListLayoutProps> {
|
||||
!cellProps.showWithColumn && (
|
||||
<MenuItem key={index} className="input">
|
||||
<Checkbox
|
||||
label={cellProps.title ?? `<${cellProps.className}>`}
|
||||
value={!this.isHiddenColumn(cellProps)}
|
||||
label = {cellProps.title ?? `<${cellProps.className}>`}
|
||||
value ={!this.isHiddenColumn(cellProps)}
|
||||
onChange={isVisible => this.updateColumnVisibility(cellProps, isVisible)}
|
||||
/>
|
||||
</MenuItem>
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
|
||||
&.checkbox {
|
||||
display: flex;
|
||||
@ -66,4 +67,4 @@
|
||||
color: $textColorAccent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<TableCellProps>;
|
||||
|
||||
@ -16,7 +17,12 @@ export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
|
||||
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 <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 (!)
|
||||
_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 (!)
|
||||
@ -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() {
|
||||
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<TableCellProps> {
|
||||
{this.renderCheckbox()}
|
||||
{_nowrap ? <div className="content">{content}</div> : content}
|
||||
{this.renderSortIcon()}
|
||||
{this.renderResizingAnchor()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -34,4 +34,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user