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:
parent
dd90dcb7f0
commit
a78bbb5f6c
@ -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);
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -12,67 +12,83 @@ 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 (
|
|
||||||
<Input
|
if (typeof value === "object") {
|
||||||
readOnly
|
return (
|
||||||
multiLine
|
<Input
|
||||||
theme="round-black"
|
readOnly
|
||||||
className="box grow"
|
multiLine
|
||||||
value={JSON.stringify(value, null, 2)}
|
theme="round-black"
|
||||||
/>
|
className="box grow"
|
||||||
);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
render() {
|
||||||
const { object } = this.props;
|
const { props: { object }, crd } = this;
|
||||||
const { crd } = this;
|
if (!object || !crd) {
|
||||||
if (!object || !crd) return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const className = cssNames("CrdResourceDetails", crd.getResourceKind());
|
const className = cssNames("CrdResourceDetails", crd.getResourceKind());
|
||||||
const extraColumns = crd.getPrinterColumns();
|
const extraColumns = crd.getPrinterColumns();
|
||||||
const showStatus = !extraColumns.find(column => column.name == "Status") && object.status?.conditions;
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<KubeObjectMeta object={object}/>
|
<KubeObjectMeta object={object} />
|
||||||
{extraColumns.map(column => {
|
{this.renderAdditionalColumns(object, extraColumns)}
|
||||||
const { name } = column;
|
{this.renderStatus(object, extraColumns)}
|
||||||
const value = jsonPath.query(object, (column.jsonPath).slice(1));
|
|
||||||
return (
|
|
||||||
<DrawerItem key={name} name={name}>
|
|
||||||
<CrdColumnValue value={value} />
|
|
||||||
</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 (
|
|
||||||
<Badge
|
|
||||||
key={kind + index} label={kind}
|
|
||||||
className={cssNames({ disabled: status === "False" }, kind.toLowerCase())}
|
|
||||||
tooltip={message}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</DrawerItem>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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 }) => (
|
||||||
|
|||||||
@ -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);
|
||||||
@ -60,4 +66,4 @@
|
|||||||
@include spinner-color(#4285F4);
|
@include spinner-color(#4285F4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}/>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()}
|
||||||
|
|||||||
18
src/renderer/utils/__tests__/display-booleans.test.tsx
Normal file
18
src/renderer/utils/__tests__/display-booleans.test.tsx
Normal 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")
|
||||||
|
})
|
||||||
|
})
|
||||||
15
src/renderer/utils/display-booleans.ts
Normal file
15
src/renderer/utils/display-booleans.ts
Normal 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
|
||||||
|
}
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user