diff --git a/src/common/user-store.ts b/src/common/user-store.ts index cf271a011d..b0294d9e5a 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -84,6 +84,15 @@ 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/api/endpoints/helm-charts.api.ts b/src/renderer/api/endpoints/helm-charts.api.ts index 8beff01779..a1fd497798 100644 --- a/src/renderer/api/endpoints/helm-charts.api.ts +++ b/src/renderer/api/endpoints/helm-charts.api.ts @@ -86,7 +86,7 @@ export class HelmChart { tillerVersion?: string; getId() { - return `${this.apiVersion}/${this.name}@${this.getAppVersion()}`; + return this.digest; } getName() { diff --git a/src/renderer/components/+workloads-pods/pods.tsx b/src/renderer/components/+workloads-pods/pods.tsx index 2296b98317..a59c9d79d2 100644 --- a/src/renderer/components/+workloads-pods/pods.tsx +++ b/src/renderer/components/+workloads-pods/pods.tsx @@ -19,8 +19,7 @@ import { lookupApiLink } from "../../api/kube-api"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { Badge } from "../badge"; - -enum sortBy { +enum columnId { name = "name", namespace = "namespace", containers = "containers", @@ -77,15 +76,15 @@ export class Pods extends React.Component { tableId = "workloads_pods" isConfigurable sortingCallbacks={{ - [sortBy.name]: (pod: Pod) => pod.getName(), - [sortBy.namespace]: (pod: Pod) => pod.getNs(), - [sortBy.containers]: (pod: Pod) => pod.getContainers().length, - [sortBy.restarts]: (pod: Pod) => pod.getRestartsCount(), - [sortBy.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind), - [sortBy.qos]: (pod: Pod) => pod.getQosClass(), - [sortBy.node]: (pod: Pod) => pod.getNodeName(), - [sortBy.age]: (pod: Pod) => pod.metadata.creationTimestamp, - [sortBy.status]: (pod: Pod) => pod.getStatusMessage(), + [columnId.name]: (pod: Pod) => pod.getName(), + [columnId.namespace]: (pod: Pod) => pod.getNs(), + [columnId.containers]: (pod: Pod) => pod.getContainers().length, + [columnId.restarts]: (pod: Pod) => pod.getRestartsCount(), + [columnId.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind), + [columnId.qos]: (pod: Pod) => pod.getQosClass(), + [columnId.node]: (pod: Pod) => pod.getNodeName(), + [columnId.age]: (pod: Pod) => pod.metadata.creationTimestamp, + [columnId.status]: (pod: Pod) => pod.getStatusMessage(), }} searchFilters={[ (pod: Pod) => pod.getSearchFields(), @@ -95,16 +94,16 @@ export class Pods extends React.Component { ]} renderHeaderTitle="Pods" renderTableHeader={[ - { title: "Name", className: "name", sortBy: sortBy.name }, - { 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 }, - { title: "Controlled By", className: "owners", sortBy: sortBy.owners }, - { title: "Node", className: "node", sortBy: sortBy.node }, - { title: "QoS", className: "qos", sortBy: sortBy.qos }, - { title: "Age", className: "age", sortBy: sortBy.age }, - { title: "Status", className: "status", sortBy: sortBy.status }, + { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, + { className: "warning", showWithColumn: columnId.name }, + { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, + { title: "Containers", className: "containers", sortBy: columnId.containers, id: columnId.containers }, + { title: "Restarts", className: "restarts", sortBy: columnId.restarts, id: columnId.restarts }, + { title: "Controlled By", className: "owners", sortBy: columnId.owners, id: columnId.owners }, + { title: "Node", className: "node", sortBy: columnId.node, id: columnId.node }, + { title: "QoS", className: "qos", sortBy: columnId.qos, id: columnId.qos }, + { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, + { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, ]} renderTableContents={(pod: Pod) => [ , 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 9bdc2f943d..0008ffd527 100644 --- a/src/renderer/components/item-object-list/item-list-layout.scss +++ b/src/renderer/components/item-object-list/item-list-layout.scss @@ -36,3 +36,14 @@ } } +.ItemListLayoutVisibilityMenu { + .MenuItem { + padding: 0; + } + + .Checkbox { + width: 100%; + padding: var(--spacing); + cursor: pointer; + } +} 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 30460733bd..aaeb7438ea 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -1,5 +1,4 @@ import "./item-list-layout.scss"; -import "./table-menu.scss"; import groupBy from "lodash/groupBy"; import React, { ReactNode } from "react"; @@ -23,7 +22,6 @@ 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 @@ -101,11 +99,10 @@ export class ItemListLayout extends React.Component { private watchDisposers: IReactionDisposer[] = []; - @observable hiddenColumnNames = new Set(); @observable isUnmounting = false; @observable userSettings: ItemListLayoutUserSettings = { - showAppliedFilters: true, + showAppliedFilters: false, }; constructor(props: ItemListLayoutProps) { @@ -122,9 +119,15 @@ export class ItemListLayout extends React.Component { } async componentDidMount() { + const { isClusterScoped, isConfigurable, tableId } = this.props; + + if (isConfigurable && !tableId) { + throw new Error("[ItemListLayout]: configurable list require props.tableId to be specified"); + } + this.loadStores(); - if (!this.props.isClusterScoped) { + if (!isClusterScoped) { disposeOnUnmount(this, [ namespaceStore.onContextChange(() => this.loadStores()) ]); @@ -137,11 +140,7 @@ export class ItemListLayout extends React.Component { } @computed get stores() { - const { store, dependentStores, tableId } = this.props; - - if (this.canBeConfigured) { - this.hiddenColumnNames = new Set(userStore.preferences?.hiddenTableColumns?.[tableId]); - } + const { store, dependentStores } = this.props; return new Set([store, ...dependentStores]); } @@ -249,42 +248,6 @@ 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 { @@ -316,20 +279,18 @@ export class ItemListLayout extends React.Component { /> )} { - renderTableContents(item) - .map((content, index) => { - const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content; + renderTableContents(item).map((content, index) => { + const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content; + const headCell = renderTableHeader?.[index]; - if (copyClassNameFromHeadCells && renderTableHeader) { - const headCell = renderTableHeader[index]; + if (copyClassNameFromHeadCells && headCell) { + cellProps.className = cssNames(cellProps.className, headCell.className); + } - if (headCell) { - cellProps.className = cssNames(cellProps.className, headCell.className); - } - } - - return this.columnIsVisible(index) ? : null; - }) + if (!headCell || !this.isHiddenColumn(headCell)) { + return ; + } + }) } {renderItemMenu && ( @@ -458,10 +419,40 @@ export class ItemListLayout extends React.Component { ); } + renderTableHeader() { + const { renderTableHeader, isSelectable, isConfigurable, store } = this.props; + + if (!renderTableHeader) { + return; + } + + return ( + + {isSelectable && ( + store.toggleSelectionAll(this.items))} + /> + )} + {renderTableHeader.map((cellProps, index) => { + if (!this.isHiddenColumn(cellProps)) { + return ; + } + })} + {isConfigurable && ( + + {this.renderColumnVisibilityMenu()} + + )} + + ); + } + renderList() { const { - isSelectable, tableProps = {}, renderTableHeader, renderItemMenu, - store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem + store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem, + tableProps = {}, } = this.props; const { isReady, removeItemsDialog, items } = this; const { selectedItems } = store; @@ -486,22 +477,7 @@ export class ItemListLayout extends React.Component { className: cssNames("box grow", tableProps.className, themeStore.activeTheme.type), })} > - {renderTableHeader && ( - - {isSelectable && ( - store.toggleSelectionAll(items))} - /> - )} - {renderTableHeader.map((cellProps, index) => this.columnIsVisible(index) ? : null)} - {renderItemMenu && - {this.canBeConfigured && this.renderColumnMenu()} - - } - - )} + {this.renderTableHeader()} { !virtual && items.map(item => this.getRow(item.getId())) } @@ -517,23 +493,44 @@ export class ItemListLayout extends React.Component { ); } - renderColumnMenu() { + @computed get hiddenColumns() { + return userStore.getHiddenTableColumns(this.props.tableId); + } + + isHiddenColumn({ id: columnId, showWithColumn }: TableCellProps): boolean { + if (!this.props.isConfigurable) { + return false; + } + + return this.hiddenColumns.has(columnId) || ( + showWithColumn && this.hiddenColumns.has(showWithColumn) + ); + } + + 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); + } + + renderColumnVisibilityMenu() { 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)} + value={!this.isHiddenColumn(cellProps)} + onChange={isVisible => this.updateColumnVisibility(cellProps, isVisible)} /> ) diff --git a/src/renderer/components/item-object-list/table-menu.scss b/src/renderer/components/item-object-list/table-menu.scss deleted file mode 100644 index b7e41f54ca..0000000000 --- a/src/renderer/components/item-object-list/table-menu.scss +++ /dev/null @@ -1,4 +0,0 @@ -.MenuCheckbox { - width: 100%; - height: 100%; -} diff --git a/src/renderer/components/table/table-cell.tsx b/src/renderer/components/table/table-cell.tsx index 97335078f1..81e2f9f85f 100644 --- a/src/renderer/components/table/table-cell.tsx +++ b/src/renderer/components/table/table-cell.tsx @@ -9,13 +9,14 @@ import { Checkbox } from "../checkbox"; export type TableCellElem = React.ReactElement; export interface TableCellProps extends React.DOMAttributes { + id?: string; // used for configuration visibility of columns className?: string; title?: ReactNode; checkbox?: boolean; // render cell with a checkbox 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 + showWithColumn?: string // id of the column which follow same visibility rules _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 (!) @@ -73,7 +74,7 @@ export class TableCell extends React.Component { const content = displayBooleans(displayBoolean, title || children); return ( -
+
{this.renderCheckbox()} {_nowrap ?
{content}
: content} {this.renderSortIcon()}