1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/src/renderer/components/item-object-list/content.tsx
2022-10-03 09:53:06 -04:00

387 lines
12 KiB
TypeScript

/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./item-list-layout.scss";
import type { ReactNode } from "react";
import React from "react";
import type { IComputedValue } from "mobx";
import { computed, makeObservable } from "mobx";
import { Observer, observer } from "mobx-react";
import type { ConfirmDialogParams } from "../confirm-dialog";
import type { TableCellProps, TableProps, TableRowProps, TableSortCallbacks } from "../table";
import { Table, TableCell, TableHead, TableRow } from "../table";
import type { IClassName } from "../../utils";
import { autoBind, cssNames, isDefined, isReactNode, noop, prevDefault, stopPropagation } from "../../utils";
import type { AddRemoveButtonsProps } from "../add-remove-buttons";
import { AddRemoveButtons } from "../add-remove-buttons";
import { NoItems } from "../no-items";
import { Spinner } from "../spinner";
import type { ItemObject } from "../../../common/item.store";
import type { Filter, PageFiltersStore } from "./page-filters/store";
import type { LensTheme } from "../../themes/store";
import { MenuActions } from "../menu/menu-actions";
import { MenuItem } from "../menu";
import { Checkbox } from "../checkbox";
import type { UserStore } from "../../../common/user-store";
import type { ItemListStore } from "./list-layout";
import { withInjectables } from "@ogre-tools/injectable-react";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import pageFiltersStoreInjectable from "./page-filters/store.injectable";
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
import openConfirmDialogInjectable from "../confirm-dialog/open.injectable";
import activeThemeInjectable from "../../themes/active.injectable";
export interface ItemListLayoutContentProps<Item extends ItemObject, PreLoadStores extends boolean> {
getFilters: () => Filter[];
tableId?: string;
className: IClassName;
getItems: () => Item[];
store: ItemListStore<Item, PreLoadStores>;
getIsReady: () => boolean; // show loading indicator while not ready
isSelectable?: boolean; // show checkbox in rows for selecting items
isConfigurable?: boolean;
copyClassNameFromHeadCells?: boolean;
sortingCallbacks?: TableSortCallbacks<Item>;
tableProps?: Partial<TableProps<Item>>; // low-level table configuration
renderTableHeader?: (TableCellProps | undefined | null)[];
renderTableContents: (item: Item) => (ReactNode | TableCellProps)[];
renderItemMenu?: (item: Item, store: ItemListStore<Item, PreLoadStores>) => ReactNode;
customizeTableRowProps?: (item: Item) => Partial<TableRowProps<Item>>;
addRemoveButtons?: Partial<AddRemoveButtonsProps>;
virtual?: boolean;
// item details view
hasDetailsView?: boolean;
detailsItem?: Item;
onDetails?: (item: Item) => void;
// other
customizeRemoveDialog?: (selectedItems: Item[]) => Partial<ConfirmDialogParams>;
spinnerTestId?: string;
/**
* Message to display when a store failed to load
*
* @default "Failed to load items"
*/
failedToLoadMessage?: React.ReactNode;
}
interface Dependencies {
activeTheme: IComputedValue<LensTheme>;
userStore: UserStore;
pageFiltersStore: PageFiltersStore;
openConfirmDialog: OpenConfirmDialog;
}
@observer
class NonInjectedItemListLayoutContent<
Item extends ItemObject,
PreLoadStores extends boolean,
> extends React.Component<ItemListLayoutContentProps<Item, PreLoadStores> & Dependencies> {
constructor(props: ItemListLayoutContentProps<Item, PreLoadStores> & Dependencies) {
super(props);
makeObservable(this);
autoBind(this);
}
@computed get failedToLoad() {
return this.props.store.failedLoading;
}
renderRow(item: Item) {
return this.getTableRow(item);
}
getTableRow(item: Item) {
const {
isSelectable, renderTableHeader, renderTableContents, renderItemMenu,
store, hasDetailsView, onDetails,
copyClassNameFromHeadCells, customizeTableRowProps = () => ({}), detailsItem,
} = this.props;
const { isSelected } = store;
return (
<TableRow
nowrap
searchItem={item}
sortItem={item}
selected={detailsItem && detailsItem.getId() === item.getId()}
onClick={hasDetailsView ? prevDefault(() => onDetails?.(item)) : undefined}
{...customizeTableRowProps(item)}
>
{isSelectable && (
<TableCell
checkbox
isChecked={isSelected(item)}
onClick={prevDefault(() => store.toggleSelection(item))}
/>
)}
{renderTableContents(item).map((content, index) => {
const cellProps: TableCellProps = isReactNode(content)
? { children: content }
: content;
const headCell = renderTableHeader?.[index];
if (copyClassNameFromHeadCells && headCell) {
cellProps.className = cssNames(
cellProps.className,
headCell.className,
);
}
if (!headCell || this.showColumn(headCell)) {
return <TableCell key={index} {...cellProps} />;
}
return null;
})}
{renderItemMenu && (
<TableCell className="menu">
<div onClick={stopPropagation}>
{renderItemMenu(item, store)}
</div>
</TableCell>
)}
</TableRow>
);
}
getRow(uid: string) {
return (
<div key={uid}>
<Observer>
{() => {
const item = this.props.getItems().find(item => item.getId() === uid);
if (!item) return null;
return this.getTableRow(item);
}}
</Observer>
</div>
);
}
removeItemsDialog(selectedItems: Item[]) {
const { customizeRemoveDialog, store, openConfirmDialog } = this.props;
const visibleMaxNamesCount = 5;
const selectedNames = selectedItems.map(ns => ns.getName()).slice(0, visibleMaxNamesCount).join(", ");
const dialogCustomProps = customizeRemoveDialog ? customizeRemoveDialog(selectedItems) : {};
const selectedCount = selectedItems.length;
const tailCount = selectedCount > visibleMaxNamesCount
? selectedCount - visibleMaxNamesCount
: 0;
const tail = tailCount > 0
? (
<>
{", and "}
<b>{tailCount}</b>
{" more"}
</>
)
: null;
const message = selectedCount <= 1
? (
<p>
{"Remove item "}
<b>{selectedNames}</b>
?
</p>
)
: (
<p>
{"Remove "}
<b>{selectedCount}</b>
{" items "}
<b>{selectedNames}</b>
{tail}
?
</p>
);
const { removeSelectedItems, removeItems } = store;
const onConfirm = typeof removeItems === "function"
? () => removeItems.apply(store, [selectedItems])
: typeof removeSelectedItems === "function"
? removeSelectedItems.bind(store)
: noop;
openConfirmDialog({
ok: onConfirm,
labelOk: "Remove",
message,
...dialogCustomProps,
});
}
renderNoItems() {
if (this.failedToLoad) {
return <NoItems>{this.props.failedToLoadMessage}</NoItems>;
}
if (!this.props.getIsReady()) {
return <Spinner center data-testid={this.props.spinnerTestId} />;
}
if (this.props.getFilters().length > 0) {
return (
<NoItems>
No items found.
<p>
<a onClick={() => this.props.pageFiltersStore.reset()} className="contrast">
Reset filters?
</a>
</p>
</NoItems>
);
}
return <NoItems />;
}
renderItems() {
if (this.props.virtual) {
return null;
}
return this.props.getItems().map(item => this.getRow(item.getId()));
}
renderTableHeader() {
const { customizeTableRowProps, renderTableHeader, isSelectable, isConfigurable, store } = this.props;
if (!renderTableHeader) {
return null;
}
const enabledItems = this.props.getItems().filter(item => !customizeTableRowProps?.(item).disabled);
return (
<TableHead showTopLine nowrap>
{isSelectable && (
<Observer>
{() => (
<TableCell
checkbox
isChecked={store.isSelectedAll(enabledItems)}
onClick={prevDefault(() => store.toggleSelectionAll(enabledItems))}
/>
)}
</Observer>
)}
{
renderTableHeader
.filter(isDefined)
.map((cellProps, index) => (
this.showColumn(cellProps) && (
<TableCell key={cellProps.id ?? index} {...cellProps} />
)
))
}
<TableCell className="menu">
{isConfigurable && this.renderColumnVisibilityMenu()}
</TableCell>
</TableHead>
);
}
render() {
const {
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks,
detailsItem, className, tableProps = {}, tableId, getItems, activeTheme,
} = this.props;
const selectedItemId = detailsItem && detailsItem.getId();
const classNames = cssNames(className, "box", "grow", activeTheme.get().type);
const items = getItems();
const selectedItems = store.pickOnlySelected(items);
return (
<div className="items box grow flex column">
<Table
tableId={tableId}
virtual={virtual}
selectable={hasDetailsView}
sortable={sortingCallbacks}
getTableRow={this.getRow}
renderRow={virtual ? undefined : this.renderRow}
items={items}
selectedItemId={selectedItemId}
noItems={this.renderNoItems()}
className={classNames}
{...tableProps}
>
{this.renderTableHeader()}
{this.renderItems()}
</Table>
<Observer>
{() => (
<AddRemoveButtons
onRemove={
(store.removeItems || store.removeSelectedItems) && selectedItems.length > 0
? () => this.removeItemsDialog(selectedItems)
: undefined
}
removeTooltip={`Remove selected items (${selectedItems.length})`}
{...addRemoveButtons}
/>
)}
</Observer>
</div>
);
}
showColumn({ id: columnId, showWithColumn }: TableCellProps): boolean {
const { tableId, isConfigurable } = this.props;
return !isConfigurable || !tableId || !this.props.userStore.isTableColumnHidden(tableId, columnId, showWithColumn);
}
renderColumnVisibilityMenu() {
const { renderTableHeader = [], tableId } = this.props;
return (
<MenuActions
id="menu-actions-for-item-object-list-content"
className="ItemListLayoutVisibilityMenu"
toolbar={false}
autoCloseOnSelect={false}
>
{
renderTableHeader
.filter(isDefined)
.map((cellProps, index) => (
!cellProps.showWithColumn && (
<MenuItem key={index} className="input">
<Checkbox
label={cellProps.title ?? `<${cellProps.className}>`}
value={this.showColumn(cellProps)}
onChange={(
tableId
? (() => cellProps.id && this.props.userStore.toggleTableColumnVisibility(tableId, cellProps.id))
: undefined
)}
/>
</MenuItem>
)
))
}
</MenuActions>
);
}
}
export const ItemListLayoutContent = withInjectables<Dependencies, ItemListLayoutContentProps<ItemObject, boolean>>(NonInjectedItemListLayoutContent, {
getProps: (di, props) => ({
...props,
activeTheme: di.inject(activeThemeInjectable),
userStore: di.inject(userStoreInjectable),
pageFiltersStore: di.inject(pageFiltersStoreInjectable),
openConfirmDialog: di.inject(openConfirmDialogInjectable),
}),
}) as <Item extends ItemObject, PreLoadStores extends boolean>(props: ItemListLayoutContentProps<Item, PreLoadStores>) => React.ReactElement;