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)
import path from "path";
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 {
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 Array.from(this.apis.values()).find(pathOrCallback);
return Array.from(this.apis.values()).find(pathOrCallback ?? ((api: KubeApi) => true));
}
registerApi(apiBase: string, api: KubeApi) {

View File

@ -9,7 +9,7 @@ type AdditionalPrinterColumnsCommon = {
description: string;
}
type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & {
export type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & {
jsonPath: string;
}
@ -120,7 +120,7 @@ export class CustomResourceDefinition extends KubeObject {
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
?? this.spec.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
?? [];
@ -149,4 +149,3 @@ export class CustomResourceDefinition extends KubeObject {
export const crdApi = new VersionedKubeApi<CustomResourceDefinition>({
objectConstructor: CustomResourceDefinition
});

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import "./drawer-item.scss";
import React from "react";
import { cssNames } from "../../utils";
import { cssNames, displayBooleans } from "../../utils";
export interface DrawerItemProps extends React.HTMLAttributes<any> {
name: React.ReactNode;
@ -8,18 +8,21 @@ export interface DrawerItemProps extends React.HTMLAttributes<any> {
title?: string;
labelsOnly?: 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> {
render() {
const { name, title, labelsOnly, children, hidden, ...elemProps } = this.props
let { className } = this.props;
const { name, title, labelsOnly, children, hidden, className, renderBoolean, ...elemProps } = this.props
if (hidden) return null
className = cssNames("DrawerItem", className, { labelsOnly });
const classNames = cssNames("DrawerItem", className, { labelsOnly });
const content = displayBooleans(renderBoolean, children)
return (
<div {...elemProps} className={className} title={title}>
<div {...elemProps} className={classNames} title={title}>
<span className="name">{name}</span>
<span className="value">{children}</span>
<span className="value">{content}</span>
</div>
)
}

View File

@ -27,8 +27,9 @@ import { crdStore } from "../+custom-resources/crd.store";
import { CrdList, crdResourcesRoute, crdRoute, crdURL } from "../+custom-resources";
import { CustomResources } from "../+custom-resources/custom-resources";
import { navigation } from "../../navigation";
import { isAllowedResource } from "../../../common/rbac"
import { clusterPageRegistry } from "../../../extensions/registries/page-registry";
import { isAllowedResource } from "../../../common/rbac";
import { Spinner } from "../spinner";
const SidebarContext = React.createContext<SidebarContextValue>({ pinned: false });
type SidebarContextValue = {
@ -50,6 +51,10 @@ export class Sidebar extends React.Component<Props> {
}
renderCustomResources() {
if (crdStore.isLoading) {
return <Spinner centerHorizontal />
}
return Object.entries(crdStore.groups).map(([group, crds]) => {
const submenus = crds.map((crd) => {
return {

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import "./table-cell.scss";
import type { TableSortBy, TableSortParams } from "./table";
import React, { ReactNode } from "react";
import { autobind, cssNames } from "../../utils";
import { autobind, cssNames, displayBooleans } from "../../utils";
import { Icon } from "../icon";
import { Checkbox } from "../checkbox";
@ -13,6 +13,7 @@ export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
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 <Table sortable={}/>
_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 (!)
@ -57,15 +58,15 @@ export class TableCell extends React.Component<TableCellProps> {
}
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, {
checkbox: checkbox,
nowrap: _nowrap,
sorting: this.isSortable,
});
const content = title || children;
const content = displayBooleans(displayBoolean, title || children)
return (
<div {...cellProps} className={classNames} onClick={this.onClick}>
<div {...cellProps} id={className} className={classNames} onClick={this.onClick}>
{this.renderCheckbox()}
{_nowrap ? <div className="content">{content}</div> : content}
{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 "./convertCpu"
export * from "./metricUnitsToNumber"
export * from "./display-booleans"

View File

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