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

Fix rendering of boolean values in CRDs (#1087)

* Fix rendering of boolean values in CRDs

- add optional special casing for boolean values in DrawerItems and
  TableRows since React (imo annoying fashion) does not render boolean
  values by default.
- add a spinner on the sidebar for when the CRD menu is expeanded but
  the entries have not been loaded yet.
- Add ability to double click a Badge to expand, also make it so that
  Badges highligh all text on first click.

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2020-11-10 10:10:19 -05:00 committed by GitHub
parent dd90dcb7f0
commit a78bbb5f6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 167 additions and 101 deletions

View File

@ -1,7 +1,8 @@
// Save file to electron app directory (e.g. "/Users/$USER/Library/Application Support/Lens" for MacOS) // Save file to electron app directory (e.g. "/Users/$USER/Library/Application Support/Lens" for MacOS)
import path from "path"; import path from "path";
import { app, remote } from "electron"; import { app, remote } from "electron";
import { ensureDirSync, writeFileSync, WriteFileOptions } from "fs-extra"; import { ensureDirSync, writeFileSync } from "fs-extra";
import { WriteFileOptions } from "fs"
export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string { export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string {
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath); const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);

View File

@ -14,7 +14,7 @@ export class ApiManager {
return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase); return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase);
} }
return Array.from(this.apis.values()).find(pathOrCallback); return Array.from(this.apis.values()).find(pathOrCallback ?? ((api: KubeApi) => true));
} }
registerApi(apiBase: string, api: KubeApi) { registerApi(apiBase: string, api: KubeApi) {

View File

@ -9,7 +9,7 @@ type AdditionalPrinterColumnsCommon = {
description: string; description: string;
} }
type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & { export type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & {
jsonPath: string; jsonPath: string;
} }
@ -120,9 +120,9 @@ export class CustomResourceDefinition extends KubeObject {
return JSON.stringify(this.spec.conversion); return JSON.stringify(this.spec.conversion);
} }
getPrinterColumns(ignorePriority = true) { getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] {
const columns = this.spec.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns const columns = this.spec.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
?? this.spec.additionalPrinterColumns?.map(({JSONPath, ...rest}) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape ?? this.spec.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
?? []; ?? [];
return columns return columns
.filter(column => column.name != "Age") .filter(column => column.name != "Age")
@ -149,4 +149,3 @@ export class CustomResourceDefinition extends KubeObject {
export const crdApi = new VersionedKubeApi<CustomResourceDefinition>({ export const crdApi = new VersionedKubeApi<CustomResourceDefinition>({
objectConstructor: CustomResourceDefinition objectConstructor: CustomResourceDefinition
}); });

View File

@ -12,16 +12,18 @@ import { KubeObjectDetailsProps } from "../kube-object";
import { crdStore } from "./crd.store"; import { crdStore } from "./crd.store";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Input } from "../input"; import { Input } from "../input";
import { CustomResourceDefinition } from "../../api/endpoints/crd.api"; import { AdditionalPrinterColumnsV1, CustomResourceDefinition } from "../../api/endpoints/crd.api";
interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> { interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
} }
function CrdColumnValue({ value }: { value: any[] | {} | string }) { function convertSpecValue(value: any): any {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return <>{value.map((item, index) => <CrdColumnValue key={index} value={item} />)}</> return value.map(convertSpecValue)
} }
if (typeof(value) === 'object') return (
if (typeof value === "object") {
return (
<Input <Input
readOnly readOnly
multiLine multiLine
@ -29,50 +31,64 @@ function CrdColumnValue({ value }: { value: any[] | {} | string }) {
className="box grow" className="box grow"
value={JSON.stringify(value, null, 2)} value={JSON.stringify(value, null, 2)}
/> />
); )
return <span>{value}</span>; }
return value
} }
@observer @observer
export class CrdResourceDetails extends React.Component<Props> { export class CrdResourceDetails extends React.Component<Props> {
@computed get crd() { @computed get crd() {
return crdStore.getByObject(this.props.object); return crdStore.getByObject(this.props.object);
} }
render() { renderAdditionalColumns(crd: CustomResourceDefinition, columns: AdditionalPrinterColumnsV1[]) {
const { object } = this.props; return columns.map(({ name, jsonPath: jp }) => (
const { crd } = this; <DrawerItem key={name} name={name} renderBoolean>
if (!object || !crd) return null; {convertSpecValue(jsonPath.value(crd, jp.slice(1)))}
const className = cssNames("CrdResourceDetails", crd.getResourceKind());
const extraColumns = crd.getPrinterColumns();
const showStatus = !extraColumns.find(column => column.name == "Status") && object.status?.conditions;
return (
<div className={className}>
<KubeObjectMeta object={object}/>
{extraColumns.map(column => {
const { name } = column;
const value = jsonPath.query(object, (column.jsonPath).slice(1));
return (
<DrawerItem key={name} name={name}>
<CrdColumnValue value={value} />
</DrawerItem> </DrawerItem>
) ))
})} }
{showStatus && (
<DrawerItem name={<Trans>Status</Trans>} className="status" labelsOnly> renderStatus(crd: CustomResourceDefinition, columns: AdditionalPrinterColumnsV1[]) {
{object.status.conditions.map((condition, index) => { const showStatus = !columns.find(column => column.name == "Status") && crd.status?.conditions;
const { type, reason, message, status } = condition; if (!showStatus) {
const kind = type || reason; return null
if (!kind) return null; }
return (
const conditions = crd.status.conditions
.filter(({ type, reason }) => type || reason)
.map(({ type, reason, message, status }) => ({ kind: type || reason, message, status }))
.map(({ kind, message, status }, index) => (
<Badge <Badge
key={kind + index} label={kind} key={kind + index} label={kind}
className={cssNames({ disabled: status === "False" }, kind.toLowerCase())} className={cssNames({ disabled: status === "False" }, kind.toLowerCase())}
tooltip={message} tooltip={message}
/> />
); ))
})}
return (
<DrawerItem name={<Trans>Status</Trans>} className="status" labelsOnly>
{conditions}
</DrawerItem> </DrawerItem>
)} )
}
render() {
const { props: { object }, crd } = this;
if (!object || !crd) {
return null;
}
const className = cssNames("CrdResourceDetails", crd.getResourceKind());
const extraColumns = crd.getPrinterColumns();
return (
<div className={className}>
<KubeObjectMeta object={object} />
{this.renderAdditionalColumns(object, extraColumns)}
{this.renderStatus(object, extraColumns)}
</div> </div>
) )
} }

View File

@ -56,11 +56,11 @@ export class CrdResources extends React.Component<Props> {
[sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp, [sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp,
} }
extraColumns.forEach(column => { extraColumns.forEach(column => {
sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.query(item, column.jsonPath.slice(1)) sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.value(item, column.jsonPath.slice(1))
}) })
const ListView = KubeObjectListLayout;
return ( return (
<ListView <KubeObjectListLayout
className="CrdResources" className="CrdResources"
isClusterScoped={!isNamespaced} isClusterScoped={!isNamespaced}
store={store} store={store}
@ -85,9 +85,10 @@ export class CrdResources extends React.Component<Props> {
renderTableContents={(crdInstance: KubeObject) => [ renderTableContents={(crdInstance: KubeObject) => [
crdInstance.getName(), crdInstance.getName(),
isNamespaced && crdInstance.getNs(), isNamespaced && crdInstance.getNs(),
...extraColumns.map(column => { ...extraColumns.map(column => ({
return jsonPath.query(crdInstance, (column.jsonPath).slice(1)) renderBoolean: true,
}), children: jsonPath.value(crdInstance, column.jsonPath.slice(1)),
})),
crdInstance.getAge(), crdInstance.getAge(),
]} ]}
/> />

View File

@ -1,6 +1,6 @@
import "./drawer-item.scss"; import "./drawer-item.scss";
import React from "react"; import React from "react";
import { cssNames } from "../../utils"; import { cssNames, displayBooleans } from "../../utils";
export interface DrawerItemProps extends React.HTMLAttributes<any> { export interface DrawerItemProps extends React.HTMLAttributes<any> {
name: React.ReactNode; name: React.ReactNode;
@ -8,18 +8,21 @@ export interface DrawerItemProps extends React.HTMLAttributes<any> {
title?: string; title?: string;
labelsOnly?: boolean; labelsOnly?: boolean;
hidden?: boolean; hidden?: boolean;
renderBoolean?: boolean; // show "true" or "false" for all of the children elements are "typeof boolean"
} }
export class DrawerItem extends React.Component<DrawerItemProps> { export class DrawerItem extends React.Component<DrawerItemProps> {
render() { render() {
const { name, title, labelsOnly, children, hidden, ...elemProps } = this.props const { name, title, labelsOnly, children, hidden, className, renderBoolean, ...elemProps } = this.props
let { className } = this.props;
if (hidden) return null if (hidden) return null
className = cssNames("DrawerItem", className, { labelsOnly });
const classNames = cssNames("DrawerItem", className, { labelsOnly });
const content = displayBooleans(renderBoolean, children)
return ( return (
<div {...elemProps} className={className} title={title}> <div {...elemProps} className={classNames} title={title}>
<span className="name">{name}</span> <span className="name">{name}</span>
<span className="value">{children}</span> <span className="value">{content}</span>
</div> </div>
) )
} }

View File

@ -235,7 +235,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
cellProps.className = cssNames(cellProps.className, headCell.className); cellProps.className = cssNames(cellProps.className, headCell.className);
} }
} }
return <TableCell key={index} {...cellProps}/> return <TableCell key={index} {...cellProps} />
}) })
} }
{renderItemMenu && ( {renderItemMenu && (
@ -277,7 +277,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
if (!isReady || !filters.length || hideFilters || !userSettings.showAppliedFilters) { if (!isReady || !filters.length || hideFilters || !userSettings.showAppliedFilters) {
return; return;
} }
return <PageFiltersList filters={filters}/> return <PageFiltersList filters={filters} />
} }
renderNoItems() { renderNoItems() {
@ -297,7 +297,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
</NoItems> </NoItems>
) )
} }
return <NoItems/> return <NoItems />
} }
renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode { renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode {
@ -344,12 +344,12 @@ 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/>} {!isClusterScoped && <NamespaceSelectFilter />}
<PageFiltersSelect allowEmpty disableFilters={{ <PageFiltersSelect allowEmpty disableFilters={{
[FilterType.NAMESPACE]: true, // namespace-select used instead [FilterType.NAMESPACE]: true, // namespace-select used instead
}}/> }} />
</>, </>,
search: <SearchInputUrl/>, search: <SearchInputUrl />,
} }
let header = this.renderHeaderContent(placeholders); let header = this.renderHeaderContent(placeholders);
if (customizeHeader) { if (customizeHeader) {
@ -381,7 +381,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
return ( return (
<div className="items box grow flex column"> <div className="items box grow flex column">
{!isReady && ( {!isReady && (
<Spinner center/> <Spinner center />
)} )}
{isReady && ( {isReady && (
<Table <Table
@ -406,8 +406,8 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
onClick={prevDefault(() => store.toggleSelectionAll(items))} onClick={prevDefault(() => store.toggleSelectionAll(items))}
/> />
)} )}
{renderTableHeader.map((cellProps, index) => <TableCell key={index} {...cellProps}/>)} {renderTableHeader.map((cellProps, index) => <TableCell key={index} {...cellProps} />)}
{renderItemMenu && <TableCell className="menu"/>} {renderItemMenu && <TableCell className="menu" />}
</TableHead> </TableHead>
)} )}
{ {

View File

@ -27,8 +27,9 @@ import { crdStore } from "../+custom-resources/crd.store";
import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resources"; import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resources";
import { CustomResources } from "../+custom-resources/custom-resources"; import { CustomResources } from "../+custom-resources/custom-resources";
import { navigation } from "../../navigation"; import { navigation } from "../../navigation";
import { isAllowedResource } from "../../../common/rbac"
import { clusterPageRegistry } from "../../../extensions/registries/page-registry"; import { clusterPageRegistry } from "../../../extensions/registries/page-registry";
import { isAllowedResource } from "../../../common/rbac";
import { Spinner } from "../spinner";
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false }); const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
type SidebarContextValue = { type SidebarContextValue = {
@ -50,6 +51,10 @@ export class Sidebar extends React.Component<Props> {
} }
renderCustomResources() { renderCustomResources() {
if (crdStore.isLoading) {
return <Spinner centerHorizontal />
}
return Object.entries(crdStore.groups).map(([group, crds]) => { return Object.entries(crdStore.groups).map(([group, crds]) => {
const submenus = crds.map((crd) => { const submenus = crds.map((crd) => {
return { return {
@ -80,7 +85,7 @@ export class Sidebar extends React.Component<Props> {
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}> <div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>
<div className="header flex align-center"> <div className="header flex align-center">
<NavLink exact to="/" className="box grow"> <NavLink exact to="/" className="box grow">
<Icon svg="logo-lens" className="logo-icon"/> <Icon svg="logo-lens" className="logo-icon" />
<div className="logo-text">Lens</div> <div className="logo-text">Lens</div>
</NavLink> </NavLink>
<Icon <Icon
@ -97,14 +102,14 @@ export class Sidebar extends React.Component<Props> {
isHidden={!isAllowedResource("nodes")} isHidden={!isAllowedResource("nodes")}
url={clusterURL()} url={clusterURL()}
text={<Trans>Cluster</Trans>} text={<Trans>Cluster</Trans>}
icon={<Icon svg="kube"/>} icon={<Icon svg="kube" />}
/> />
<SidebarNavItem <SidebarNavItem
id="nodes" id="nodes"
isHidden={!isAllowedResource("nodes")} isHidden={!isAllowedResource("nodes")}
url={nodesURL()} url={nodesURL()}
text={<Trans>Nodes</Trans>} text={<Trans>Nodes</Trans>}
icon={<Icon svg="nodes"/>} icon={<Icon svg="nodes" />}
/> />
<SidebarNavItem <SidebarNavItem
id="workloads" id="workloads"
@ -113,7 +118,7 @@ export class Sidebar extends React.Component<Props> {
routePath={workloadsRoute.path} routePath={workloadsRoute.path}
subMenus={Workloads.tabRoutes} subMenus={Workloads.tabRoutes}
text={<Trans>Workloads</Trans>} text={<Trans>Workloads</Trans>}
icon={<Icon svg="workloads"/>} icon={<Icon svg="workloads" />}
/> />
<SidebarNavItem <SidebarNavItem
id="config" id="config"
@ -122,7 +127,7 @@ export class Sidebar extends React.Component<Props> {
routePath={configRoute.path} routePath={configRoute.path}
subMenus={Config.tabRoutes} subMenus={Config.tabRoutes}
text={<Trans>Configuration</Trans>} text={<Trans>Configuration</Trans>}
icon={<Icon material="list"/>} icon={<Icon material="list" />}
/> />
<SidebarNavItem <SidebarNavItem
id="networks" id="networks"
@ -131,7 +136,7 @@ export class Sidebar extends React.Component<Props> {
routePath={networkRoute.path} routePath={networkRoute.path}
subMenus={Network.tabRoutes} subMenus={Network.tabRoutes}
text={<Trans>Network</Trans>} text={<Trans>Network</Trans>}
icon={<Icon material="device_hub"/>} icon={<Icon material="device_hub" />}
/> />
<SidebarNavItem <SidebarNavItem
id="storage" id="storage"
@ -139,14 +144,14 @@ export class Sidebar extends React.Component<Props> {
url={storageURL({ query })} url={storageURL({ query })}
routePath={storageRoute.path} routePath={storageRoute.path}
subMenus={Storage.tabRoutes} subMenus={Storage.tabRoutes}
icon={<Icon svg="storage"/>} icon={<Icon svg="storage" />}
text={<Trans>Storage</Trans>} text={<Trans>Storage</Trans>}
/> />
<SidebarNavItem <SidebarNavItem
id="namespaces" id="namespaces"
isHidden={!isAllowedResource("namespaces")} isHidden={!isAllowedResource("namespaces")}
url={namespacesURL()} url={namespacesURL()}
icon={<Icon material="layers"/>} icon={<Icon material="layers" />}
text={<Trans>Namespaces</Trans>} text={<Trans>Namespaces</Trans>}
/> />
<SidebarNavItem <SidebarNavItem
@ -154,7 +159,7 @@ export class Sidebar extends React.Component<Props> {
isHidden={!isAllowedResource("events")} isHidden={!isAllowedResource("events")}
url={eventsURL({ query })} url={eventsURL({ query })}
routePath={eventRoute.path} routePath={eventRoute.path}
icon={<Icon material="access_time"/>} icon={<Icon material="access_time" />}
text={<Trans>Events</Trans>} text={<Trans>Events</Trans>}
/> />
<SidebarNavItem <SidebarNavItem
@ -162,7 +167,7 @@ export class Sidebar extends React.Component<Props> {
url={appsURL({ query })} url={appsURL({ query })}
subMenus={Apps.tabRoutes} subMenus={Apps.tabRoutes}
routePath={appsRoute.path} routePath={appsRoute.path}
icon={<Icon material="apps"/>} icon={<Icon material="apps" />}
text={<Trans>Apps</Trans>} text={<Trans>Apps</Trans>}
/> />
<SidebarNavItem <SidebarNavItem
@ -170,7 +175,7 @@ export class Sidebar extends React.Component<Props> {
url={usersManagementURL({ query })} url={usersManagementURL({ query })}
routePath={usersManagementRoute.path} routePath={usersManagementRoute.path}
subMenus={UserManagement.tabRoutes} subMenus={UserManagement.tabRoutes}
icon={<Icon material="security"/>} icon={<Icon material="security" />}
text={<Trans>Access Control</Trans>} text={<Trans>Access Control</Trans>}
/> />
<SidebarNavItem <SidebarNavItem
@ -179,7 +184,7 @@ export class Sidebar extends React.Component<Props> {
url={crdURL()} url={crdURL()}
subMenus={CustomResources.tabRoutes} subMenus={CustomResources.tabRoutes}
routePath={crdRoute.path} routePath={crdRoute.path}
icon={<Icon material="extension"/>} icon={<Icon material="extension" />}
text={<Trans>Custom Resources</Trans>} text={<Trans>Custom Resources</Trans>}
> >
{this.renderCustomResources()} {this.renderCustomResources()}
@ -194,7 +199,7 @@ export class Sidebar extends React.Component<Props> {
url={url} url={url}
routePath={path} routePath={path}
text={title} text={title}
icon={<MenuIcon/>} icon={<MenuIcon />}
/> />
) )
})} })}
@ -257,7 +262,7 @@ class SidebarNavItem extends React.Component<SidebarNavItemProps> {
<div className={cssNames("nav-item", { active: isActive })} onClick={this.toggleSubMenu}> <div className={cssNames("nav-item", { active: isActive })} onClick={this.toggleSubMenu}>
{icon} {icon}
<span className="link-text">{text}</span> <span className="link-text">{text}</span>
<Icon className="expand-icon" material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"}/> <Icon className="expand-icon" material={this.isExpanded ? "keyboard_arrow_up" : "keyboard_arrow_down"} />
</div> </div>
<ul className={cssNames("sub-menu", { active: isActive })}> <ul className={cssNames("sub-menu", { active: isActive })}>
{subMenus.map(({ title, url }) => ( {subMenus.map(({ title, url }) => (

View File

@ -34,6 +34,12 @@
margin-top: calc(var(--spinner-size) / -2); margin-top: calc(var(--spinner-size) / -2);
} }
&.centerHorizontal {
position: absolute;
left: 50%;
margin-left: calc(var(--spinner-size) / -2);
}
@keyframes rotate { @keyframes rotate {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);

View File

@ -6,23 +6,19 @@ import { cssNames } from "../../utils";
export interface SpinnerProps extends React.HTMLProps<any> { export interface SpinnerProps extends React.HTMLProps<any> {
singleColor?: boolean; singleColor?: boolean;
center?: boolean; center?: boolean;
centerHorizontal?: boolean;
} }
export class Spinner extends React.Component<SpinnerProps, {}> { export class Spinner extends React.Component<SpinnerProps, {}> {
private elem: HTMLElement;
static defaultProps = { static defaultProps = {
singleColor: true, singleColor: true,
center: false, center: false,
}; };
render() { render() {
const { center, singleColor, ...props } = this.props; const { center, singleColor, centerHorizontal, className, ...props } = this.props;
let { className } = this.props; const classNames = cssNames('Spinner', className, { singleColor, center, centerHorizontal });
className = cssNames('Spinner', className, {
singleColor: singleColor, return <div {...props} className={classNames} />;
center: center,
});
return <div {...props} className={className} ref={e => this.elem = e}/>;
} }
} }

View File

@ -2,7 +2,7 @@ import "./table-cell.scss";
import type { TableSortBy, TableSortParams } from "./table"; import type { TableSortBy, TableSortParams } from "./table";
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
import { autobind, cssNames } from "../../utils"; import { autobind, cssNames, displayBooleans } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Checkbox } from "../checkbox"; import { Checkbox } from "../checkbox";
@ -13,6 +13,7 @@ export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
title?: ReactNode; title?: ReactNode;
checkbox?: boolean; // render cell with a checkbox checkbox?: boolean; // render cell with a checkbox
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"
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={}/>
_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 (!)
@ -52,20 +53,20 @@ export class TableCell extends React.Component<TableCellProps> {
const { checkbox, isChecked } = this.props; const { checkbox, isChecked } = this.props;
const showCheckbox = isChecked !== undefined; const showCheckbox = isChecked !== undefined;
if (checkbox && showCheckbox) { if (checkbox && showCheckbox) {
return <Checkbox value={isChecked}/> return <Checkbox value={isChecked} />
} }
} }
render() { render() {
const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, ...cellProps } = this.props; const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, renderBoolean: displayBoolean, ...cellProps } = this.props;
const classNames = cssNames("TableCell", className, { const classNames = cssNames("TableCell", className, {
checkbox: checkbox, checkbox: checkbox,
nowrap: _nowrap, nowrap: _nowrap,
sorting: this.isSortable, sorting: this.isSortable,
}); });
const content = title || children; const content = displayBooleans(displayBoolean, title || children)
return ( return (
<div {...cellProps} className={classNames} onClick={this.onClick}> <div {...cellProps} id={className} className={classNames} onClick={this.onClick}>
{this.renderCheckbox()} {this.renderCheckbox()}
{_nowrap ? <div className="content">{content}</div> : content} {_nowrap ? <div className="content">{content}</div> : content}
{this.renderSortIcon()} {this.renderSortIcon()}

View File

@ -0,0 +1,18 @@
import React from "react"
import { displayBooleans } from "../display-booleans"
describe("displayBooleans tests", () => {
it("should not do anything to div's if shouldShow is false", () => {
expect(displayBooleans(false, <div></div>)).toStrictEqual(<div></div>)
})
it("should not do anything to booleans's if shouldShow is false", () => {
expect(displayBooleans(false, true)).toStrictEqual(true)
expect(displayBooleans(false, false)).toStrictEqual(false)
})
it("should stringify booleans when shouldShow is true", () => {
expect(displayBooleans(true, true)).toStrictEqual("true")
expect(displayBooleans(true, false)).toStrictEqual("false")
})
})

View File

@ -0,0 +1,15 @@
import React from "react"
export function displayBooleans(shouldShow: boolean, from: React.ReactNode): React.ReactNode {
if (shouldShow) {
if (typeof from === "boolean") {
return from.toString()
}
if (Array.isArray(from)) {
return from.map(node => displayBooleans(shouldShow, node))
}
}
return from
}

View File

@ -18,3 +18,4 @@ export * from "./isReactNode"
export * from "./convertMemory" export * from "./convertMemory"
export * from "./convertCpu" export * from "./convertCpu"
export * from "./metricUnitsToNumber" export * from "./metricUnitsToNumber"
export * from "./display-booleans"

View File

@ -4,7 +4,11 @@
"jsx": "react", "jsx": "react",
"target": "ES2017", "target": "ES2017",
"module": "ESNext", "module": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"], "lib": [
"ESNext",
"DOM",
"DOM.Iterable"
],
"moduleResolution": "Node", "moduleResolution": "Node",
"sourceMap": true, "sourceMap": true,
"strict": false, "strict": false,