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

Add copy buttons for string DrawerItem's

- Changes <LocaleDate> to localizeDate() so that the string check passes

- Changes the rendering of booleans in CRDs to manually stringify them

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-10-22 15:04:45 -04:00
parent 169f443c3b
commit deefdc9af2
13 changed files with 99 additions and 86 deletions

View File

@ -155,8 +155,11 @@ export function isMetricsEmpty(metrics: Record<string, IMetrics>) {
return Object.values(metrics).every(metric => !metric?.data?.result?.length); return Object.values(metrics).every(metric => !metric?.data?.result?.length);
} }
export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> | void { export function getItemMetrics(metrics: Record<string, IMetrics>, itemName: string): Record<string, IMetrics> | undefined {
if (!metrics) return; if (!metrics) {
return undefined;
}
const itemMetrics = { ...metrics }; const itemMetrics = { ...metrics };
for (const metric in metrics) { for (const metric in metrics) {

View File

@ -56,6 +56,10 @@ function convertSpecValue(value: any): any {
); );
} }
if (typeof value === "boolean") {
return value.toString();
}
return value; return value;
} }
@ -63,7 +67,7 @@ function convertSpecValue(value: any): any {
export class CrdResourceDetails extends React.Component<Props> { export class CrdResourceDetails extends React.Component<Props> {
renderAdditionalColumns(resource: KubeObject, columns: AdditionalPrinterColumnsV1[]) { renderAdditionalColumns(resource: KubeObject, columns: AdditionalPrinterColumnsV1[]) {
return columns.map(({ name, jsonPath: jp }) => ( return columns.map(({ name, jsonPath: jp }) => (
<DrawerItem key={name} name={name} renderBoolean> <DrawerItem key={name} name={name}>
{convertSpecValue(jsonPath.value(resource, parseJsonPath(jp.slice(1))))} {convertSpecValue(jsonPath.value(resource, parseJsonPath(jp.slice(1))))}
</DrawerItem> </DrawerItem>
)); ));

View File

@ -100,32 +100,25 @@ export class CrdResources extends React.Component<Props> {
renderTableHeader={[ renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
isNamespaced && { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, isNamespaced && { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
...extraColumns.map(column => { ...extraColumns.map(({ name }) => ({
const { name } = column; title: name,
className: name.toLowerCase(),
return { sortBy: name,
title: name, id: name
className: name.toLowerCase(), })),
sortBy: name,
id: name
};
}),
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]} ]}
renderTableContents={crdInstance => [ renderTableContents={crdInstance => [
crdInstance.getName(), crdInstance.getName(),
isNamespaced && crdInstance.getNs(), isNamespaced && crdInstance.getNs(),
...extraColumns.map((column) => { ...extraColumns.map((column) => {
let value = jsonPath.value(crdInstance, parseJsonPath(column.jsonPath.slice(1))); const value = jsonPath.value(crdInstance, parseJsonPath(column.jsonPath.slice(1)));
if (Array.isArray(value) || typeof value === "object") { if (typeof value === "object") {
value = JSON.stringify(value); return JSON.stringify(value);
} }
return { return value.toString();
renderBoolean: true,
children: value,
};
}), }),
crdInstance.getAge(), crdInstance.getAge(),
]} ]}

View File

@ -30,10 +30,10 @@ import type { KubeObjectDetailsProps } from "../kube-object-details";
import { KubeEvent } from "../../../common/k8s-api/endpoints/events.api"; import { KubeEvent } from "../../../common/k8s-api/endpoints/events.api";
import { KubeObjectMeta } from "../kube-object-meta"; import { KubeObjectMeta } from "../kube-object-meta";
import { Table, TableCell, TableHead, TableRow } from "../table"; import { Table, TableCell, TableHead, TableRow } from "../table";
import { LocaleDate } from "../locale-date";
import { getDetailsUrl } from "../kube-detail-params"; import { getDetailsUrl } from "../kube-detail-params";
import { apiManager } from "../../../common/k8s-api/api-manager"; import { apiManager } from "../../../common/k8s-api/api-manager";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { localizeDate } from "../../utils";
interface Props extends KubeObjectDetailsProps<KubeEvent> { interface Props extends KubeObjectDetailsProps<KubeEvent> {
} }
@ -70,10 +70,10 @@ export class EventDetails extends React.Component<Props> {
{event.getSource()} {event.getSource()}
</DrawerItem> </DrawerItem>
<DrawerItem name="First seen"> <DrawerItem name="First seen">
{event.getFirstSeenTime()} ago (<LocaleDate date={event.firstTimestamp} />) {event.getFirstSeenTime()} ago ({localizeDate(event.firstTimestamp)})
</DrawerItem> </DrawerItem>
<DrawerItem name="Last seen"> <DrawerItem name="Last seen">
{event.getLastSeenTime()} ago (<LocaleDate date={event.lastTimestamp} />) {event.getLastSeenTime()} ago ({localizeDate(event.lastTimestamp)})
</DrawerItem> </DrawerItem>
<DrawerItem name="Count"> <DrawerItem name="Count">
{count} {count}

View File

@ -25,8 +25,7 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { KubeObject } from "../../../common/k8s-api/kube-object"; import { KubeObject } from "../../../common/k8s-api/kube-object";
import { DrawerItem, DrawerTitle } from "../drawer"; import { DrawerItem, DrawerTitle } from "../drawer";
import { cssNames } from "../../utils"; import { cssNames, localizeDate } from "../../utils";
import { LocaleDate } from "../locale-date";
import { eventStore } from "./event.store"; import { eventStore } from "./event.store";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
@ -87,7 +86,7 @@ export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
{involvedObject.fieldPath} {involvedObject.fieldPath}
</DrawerItem> </DrawerItem>
<DrawerItem name="Last seen"> <DrawerItem name="Last seen">
<LocaleDate date={lastTimestamp} /> {localizeDate(lastTimestamp)}
</DrawerItem> </DrawerItem>
</div> </div>
); );

View File

@ -24,7 +24,7 @@ import "./pod-details-container.scss";
import React from "react"; import React from "react";
import type { IPodContainer, IPodContainerStatus, Pod } from "../../../common/k8s-api/endpoints"; import type { IPodContainer, IPodContainerStatus, Pod } from "../../../common/k8s-api/endpoints";
import { DrawerItem } from "../drawer"; import { DrawerItem } from "../drawer";
import { cssNames } from "../../utils"; import { cssNames, localizeDate } from "../../utils";
import { StatusBrick } from "../status-brick"; import { StatusBrick } from "../status-brick";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { ContainerEnvironment } from "./pod-container-env"; import { ContainerEnvironment } from "./pod-container-env";
@ -32,7 +32,6 @@ import { PodContainerPort } from "./pod-container-port";
import { ResourceMetrics } from "../resource-metrics"; import { ResourceMetrics } from "../resource-metrics";
import type { IMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; import type { IMetrics } from "../../../common/k8s-api/endpoints/metrics.api";
import { ContainerCharts } from "./container-charts"; import { ContainerCharts } from "./container-charts";
import { LocaleDate } from "../locale-date";
import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { ClusterMetricsResourceType } from "../../../common/cluster-types";
import { portForwardStore } from "../../port-forward/port-forward.store"; import { portForwardStore } from "../../port-forward/port-forward.store";
@ -70,8 +69,8 @@ export class PodDetailsContainer extends React.Component<Props> {
<span> <span>
{lastState}<br/> {lastState}<br/>
Reason: {status.lastState.terminated.reason} - exit code: {status.lastState.terminated.exitCode}<br/> Reason: {status.lastState.terminated.reason} - exit code: {status.lastState.terminated.exitCode}<br/>
Started at: {<LocaleDate date={status.lastState.terminated.startedAt} />}<br/> Started at: {localizeDate(status.lastState.terminated.startedAt)}<br/>
Finished at: {<LocaleDate date={status.lastState.terminated.finishedAt} />}<br/> Finished at: {localizeDate(status.lastState.terminated.finishedAt)}<br/>
</span> </span>
); );
} }

View File

@ -32,6 +32,17 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
color: $drawerItemNameColor; color: $drawerItemNameColor;
.Icon {
opacity: 0;
font-weight: normal;
margin-left: 8px;
}
&:hover .Icon {
opacity: 1;
transition: opacity 250ms;
}
} }
> .value { > .value {
@ -40,6 +51,10 @@
max-width: 100%; max-width: 100%;
min-width: 0; min-width: 0;
&.noCopy {
user-select: none;
}
&:empty:after { &:empty:after {
content: '' content: ''
} }

View File

@ -21,7 +21,9 @@
import "./drawer-item.scss"; import "./drawer-item.scss";
import React from "react"; import React from "react";
import { cssNames, displayBooleans } from "../../utils"; import { clipboard } from "electron";
import { cssNames } from "../../utils";
import { Icon } from "../icon";
export interface DrawerItemProps extends React.HTMLAttributes<any> { export interface DrawerItemProps extends React.HTMLAttributes<any> {
name: React.ReactNode; name: React.ReactNode;
@ -29,22 +31,49 @@ 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> { interface State {
isCopied: boolean;
}
export class DrawerItem extends React.Component<DrawerItemProps, State> {
state = {
isCopied: false,
};
copyValue = (content: string) => {
clipboard.writeText(content);
this.setState({ isCopied: true });
setTimeout(() => {
this.setState({ isCopied: false });
}, 3000);
};
render() { render() {
const { name, title, labelsOnly, children, hidden, className, renderBoolean, ...elemProps } = this.props; const { name, title, labelsOnly, children, hidden, className, ...elemProps } = this.props;
const { isCopied } = this.state;
if (hidden) return null; if (hidden) {
return null;
}
const classNames = cssNames("DrawerItem", className, { labelsOnly }); const stringChildren = React.Children.toArray(children).filter(child => typeof child === "string");
const content = displayBooleans(renderBoolean, children); const canCopy = stringChildren.length > 0;
return ( return (
<div {...elemProps} className={classNames} title={title}> <div {...elemProps} className={cssNames("DrawerItem", className, { labelsOnly })} title={title}>
<span className="name">{name}</span> <span className="name">
<span className="value">{content}</span> {name}
{canCopy && (
<Icon
material={isCopied ? "done" : "content_copy"}
tooltip={isCopied ? "Copied!" : "Copy"}
onClick={() => this.copyValue(stringChildren.join(""))}
/>
)}
</span>
<span className={cssNames("value", { noCopy: canCopy })}>{children}</span>
</div> </div>
); );
} }

View File

@ -25,9 +25,9 @@ import { DrawerItem, DrawerItemLabels } from "../drawer";
import { apiManager } from "../../../common/k8s-api/api-manager"; import { apiManager } from "../../../common/k8s-api/api-manager";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { LocaleDate } from "../locale-date";
import { getDetailsUrl } from "../kube-detail-params"; import { getDetailsUrl } from "../kube-detail-params";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { localizeDate } from "../../utils";
export interface KubeObjectMetaProps { export interface KubeObjectMetaProps {
object: KubeObject; object: KubeObject;
@ -67,7 +67,7 @@ export class KubeObjectMeta extends React.Component<KubeObjectMetaProps> {
return ( return (
<> <>
<DrawerItem name="Created" hidden={this.isHidden("creationTimestamp")}> <DrawerItem name="Created" hidden={this.isHidden("creationTimestamp")}>
{getAge(true, false)} ago ({<LocaleDate date={creationTimestamp} />}) {getAge(true, false)} ago ({localizeDate(creationTimestamp)})
</DrawerItem> </DrawerItem>
<DrawerItem name="Name" hidden={this.isHidden("name")}> <DrawerItem name="Name" hidden={this.isHidden("name")}>
{getName()} {getName()}

View File

@ -1,38 +0,0 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import { observer } from "mobx-react";
import moment from "moment-timezone";
import { UserStore } from "../../../common/user-store";
interface Props {
date: string
}
@observer
export class LocaleDate extends React.Component<Props> {
render() {
const { date } = this.props;
return moment.tz(date, UserStore.getInstance().localeTimezone).format();
}
}

View File

@ -23,7 +23,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 { boundMethod, cssNames, displayBooleans } from "../../utils"; import { boundMethod, cssNames } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Checkbox } from "../checkbox"; import { Checkbox } from "../checkbox";
@ -35,7 +35,6 @@ 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={}/>
showWithColumn?: string // id of the column which follow same visibility rules showWithColumn?: string // id of the column which follow same visibility rules
_sorting?: Partial<TableSortParams>; // <Table> sorting state, don't use this prop outside (!) _sorting?: Partial<TableSortParams>; // <Table> sorting state, don't use this prop outside (!)
@ -88,13 +87,13 @@ export class TableCell extends React.Component<TableCellProps> {
} }
render() { render() {
const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, renderBoolean: displayBoolean, showWithColumn, ...cellProps } = this.props; const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, showWithColumn, ...cellProps } = this.props;
const classNames = cssNames("TableCell", className, { const classNames = cssNames("TableCell", className, {
checkbox, checkbox,
nowrap: _nowrap, nowrap: _nowrap,
sorting: this.isSortable, sorting: this.isSortable,
}); });
const content = displayBooleans(displayBoolean, title || children); const content = title || children;
return ( return (
<div {...cellProps} className={classNames} onClick={this.onClick}> <div {...cellProps} className={classNames} onClick={this.onClick}>

View File

@ -37,3 +37,4 @@ export * from "./name-parts";
export * from "./prevDefault"; export * from "./prevDefault";
export * from "./saveFile"; export * from "./saveFile";
export * from "./storageHelper"; export * from "./storageHelper";
export * from "./locale-date";

View File

@ -19,4 +19,13 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
export * from "./locale-date"; import moment from "moment";
import { UserStore } from "../../common/user-store";
/**
* Parse `date` and reformat it based upon the settings from `UserStore`
* @param date The date string
*/
export function localizeDate(date: string): string {
return moment.tz(date, UserStore.getInstance().localeTimezone).format();
}