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

Fix all Age, LastSeen, and FirstSeen displays (#4990)

This commit is contained in:
Sebastian Malton 2022-03-17 15:17:03 -04:00 committed by GitHub
parent c9cab22558
commit b08daa811d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 458 additions and 344 deletions

View File

@ -235,6 +235,7 @@
"mobx": "^6.3.7", "mobx": "^6.3.7",
"mobx-observable-history": "^2.0.3", "mobx-observable-history": "^2.0.3",
"mobx-react": "^7.2.1", "mobx-react": "^7.2.1",
"mobx-utils": "^6.0.4",
"mock-fs": "^5.1.2", "mock-fs": "^5.1.2",
"moment": "^2.29.1", "moment": "^2.29.1",
"moment-timezone": "^0.5.34", "moment-timezone": "^0.5.34",

View File

@ -49,12 +49,18 @@ export class KubeEvent extends KubeObject {
return `${component} ${host || ""}`; return `${component} ${host || ""}`;
} }
/**
* @deprecated This function is not reactive to changing of time. If rendering use `<ReactiveDuration />` instead
*/
getFirstSeenTime() { getFirstSeenTime() {
const diff = moment().diff(this.firstTimestamp); const diff = moment().diff(this.firstTimestamp);
return formatDuration(diff, true); return formatDuration(diff, true);
} }
/**
* @deprecated This function is not reactive to changing of time. If rendering use `<ReactiveDuration />` instead
*/
getLastSeenTime() { getLastSeenTime() {
const diff = moment().diff(this.lastTimestamp); const diff = moment().diff(this.lastTimestamp);

View File

@ -261,10 +261,28 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
return this.metadata.namespace || undefined; return this.metadata.namespace || undefined;
} }
/**
* This function computes the number of milliseconds from the UNIX EPOCH to the
* creation timestamp of this object.
*/
getCreationTimestamp() {
return new Date(this.metadata.creationTimestamp).getTime();
}
/**
* @deprecated This function computes a new "now" on every call which might cause subtle issues if called multiple times
*
* NOTE: Generally you can use `getCreationTimestamp` instead.
*/
getTimeDiffFromNow(): number { getTimeDiffFromNow(): number {
return Date.now() - new Date(this.metadata.creationTimestamp).getTime(); return Date.now() - new Date(this.metadata.creationTimestamp).getTime();
} }
/**
* @deprecated This function computes a new "now" on every call might cause subtle issues if called multiple times
*
* NOTE: this function also is not reactive to updates in the current time so it should not be used for renderering
*/
getAge(humanize = true, compact = true, fromNow = false): string | number { getAge(humanize = true, compact = true, fromNow = false): string | number {
if (fromNow) { if (fromNow) {
return moment(this.metadata.creationTimestamp).fromNow(); // "string", getTimeDiffFromNow() cannot be used return moment(this.metadata.creationTimestamp).fromNow(); // "string", getTimeDiffFromNow() cannot be used

View File

@ -19,6 +19,7 @@ import { Spinner } from "../spinner";
import { ThemeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params"; import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params";
import { apiManager } from "../../../common/k8s-api/api-manager"; import { apiManager } from "../../../common/k8s-api/api-manager";
import { KubeObjectAge } from "../kube-object/age";
export interface ClusterIssuesProps { export interface ClusterIssuesProps {
className?: string; className?: string;
@ -28,8 +29,8 @@ interface IWarning extends ItemObject {
kind: string; kind: string;
message: string; message: string;
selfLink: string; selfLink: string;
age: string | number; renderAge: () => React.ReactElement;
timeDiffFromNow: number; ageMs: number;
} }
enum sortBy { enum sortBy {
@ -40,63 +41,42 @@ enum sortBy {
@observer @observer
export class ClusterIssues extends React.Component<ClusterIssuesProps> { export class ClusterIssues extends React.Component<ClusterIssuesProps> {
private sortCallbacks = {
[sortBy.type]: (warning: IWarning) => warning.kind,
[sortBy.object]: (warning: IWarning) => warning.getName(),
[sortBy.age]: (warning: IWarning) => warning.timeDiffFromNow,
};
constructor(props: ClusterIssuesProps) { constructor(props: ClusterIssuesProps) {
super(props); super(props);
makeObservable(this); makeObservable(this);
} }
@computed get warnings() { @computed get warnings(): IWarning[] {
const warnings: IWarning[] = []; return [
...nodesStore.items.flatMap(node => (
// Node bad conditions node.getWarningConditions()
nodesStore.items.forEach(node => { .map(({ message }) => ({
const { kind, selfLink, getId, getName, getAge, getTimeDiffFromNow } = node; selfLink: node.selfLink,
getId: node.getId,
node.getWarningConditions().forEach(({ message }) => { getName: node.getName,
warnings.push({ kind: node.kind,
age: getAge(), message,
getId, renderAge: () => <KubeObjectAge key="age" object={node} />,
getName, ageMs: -node.getCreationTimestamp(),
timeDiffFromNow: getTimeDiffFromNow(), }))
kind, )),
message, ...eventStore.getWarnings().map(warning => ({
selfLink, getId: () => warning.involvedObject.uid,
}); getName: () => warning.involvedObject.name,
}); renderAge: () => <KubeObjectAge key="age" object={warning} />,
}); ageMs: -warning.getCreationTimestamp(),
message: warning.message,
// Warning events for Workloads kind: warning.kind,
const events = eventStore.getWarnings(); selfLink: apiManager.lookupApiLink(warning.involvedObject, warning),
})),
events.forEach(error => { ];
const { message, involvedObject, getAge, getTimeDiffFromNow } = error;
const { uid, name, kind } = involvedObject;
warnings.push({
getId: () => uid,
getName: () => name,
timeDiffFromNow: getTimeDiffFromNow(),
age: getAge(),
message,
kind,
selfLink: apiManager.lookupApiLink(involvedObject, error),
});
});
return warnings;
} }
@boundMethod @boundMethod
getTableRow(uid: string) { getTableRow(uid: string) {
const { warnings } = this; const { warnings } = this;
const warning = warnings.find(warn => warn.getId() == uid); const warning = warnings.find(warn => warn.getId() == uid);
const { getId, getName, message, kind, selfLink, age } = warning; const { getId, getName, message, kind, selfLink, renderAge } = warning;
return ( return (
<TableRow <TableRow
@ -115,7 +95,7 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
{kind} {kind}
</TableCell> </TableCell>
<TableCell className="age"> <TableCell className="age">
{age} {renderAge()}
</TableCell> </TableCell>
</TableRow> </TableRow>
); );
@ -143,15 +123,18 @@ export class ClusterIssues extends React.Component<ClusterIssuesProps> {
return ( return (
<> <>
<SubHeader className={styles.SubHeader}> <SubHeader className={styles.SubHeader}>
<Icon material="error_outline"/>{" "} <Icon material="error_outline"/> Warnings: {warnings.length}
<>Warnings: {warnings.length}</>
</SubHeader> </SubHeader>
<Table <Table
tableId="cluster_issues" tableId="cluster_issues"
items={warnings} items={warnings}
virtual virtual
selectable selectable
sortable={this.sortCallbacks} sortable={{
[sortBy.type]: warning => warning.kind,
[sortBy.object]: warning => warning.getName(),
[sortBy.age]: warning => warning.ageMs,
}}
sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }} sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }}
sortSyncWithUrl={false} sortSyncWithUrl={false}
getTableRow={this.getTableRow} getTableRow={this.getTableRow}

View File

@ -15,6 +15,7 @@ import { Badge } from "../badge";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { HpaRouteParams } from "../../../common/routes"; import type { HpaRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -49,17 +50,18 @@ export class HorizontalPodAutoscalers extends React.Component<HorizontalPodAutos
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="configuration_hpa" tableId="configuration_hpa"
className="HorizontalPodAutoscalers" store={hpaStore} className="HorizontalPodAutoscalers"
store={hpaStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: item => item.getName(), [columnId.name]: hpa => hpa.getName(),
[columnId.namespace]: item => item.getNs(), [columnId.namespace]: hpa => hpa.getNs(),
[columnId.minPods]: item => item.getMinPods(), [columnId.minPods]: hpa => hpa.getMinPods(),
[columnId.maxPods]: item => item.getMaxPods(), [columnId.maxPods]: hpa => hpa.getMaxPods(),
[columnId.replicas]: item => item.getReplicas(), [columnId.replicas]: hpa => hpa.getReplicas(),
[columnId.age]: item => item.getTimeDiffFromNow(), [columnId.age]: hpa => -hpa.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
item => item.getSearchFields(), hpa => hpa.getSearchFields(),
]} ]}
renderHeaderTitle="Horizontal Pod Autoscalers" renderHeaderTitle="Horizontal Pod Autoscalers"
renderTableHeader={[ renderTableHeader={[
@ -81,11 +83,10 @@ export class HorizontalPodAutoscalers extends React.Component<HorizontalPodAutos
hpa.getMinPods(), hpa.getMinPods(),
hpa.getMaxPods(), hpa.getMaxPods(),
hpa.getReplicas(), hpa.getReplicas(),
hpa.getAge(), <KubeObjectAge key="age" object={hpa} />,
hpa.getConditions().map(({ type, tooltip, isReady }) => { hpa.getConditions()
if (!isReady) return null; .filter(({ isReady }) => isReady)
.map(({ type, tooltip }) => (
return (
<Badge <Badge
key={type} key={type}
label={type} label={type}
@ -94,8 +95,7 @@ export class HorizontalPodAutoscalers extends React.Component<HorizontalPodAutos
expandable={false} expandable={false}
scrollable={true} scrollable={true}
/> />
); )),
}),
]} ]}
/> />
); );

View File

@ -12,6 +12,7 @@ import { limitRangeStore } from "./limit-ranges.store";
import React from "react"; import React from "react";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { LimitRangeRouteParams } from "../../../common/routes"; import type { LimitRangeRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -32,9 +33,9 @@ export class LimitRanges extends React.Component<LimitRangesProps> {
className="LimitRanges" className="LimitRanges"
store={limitRangeStore} store={limitRangeStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: item => item.getName(), [columnId.name]: limitRange => limitRange.getName(),
[columnId.namespace]: item => item.getNs(), [columnId.namespace]: limitRange => limitRange.getNs(),
[columnId.age]: item => item.getTimeDiffFromNow(), [columnId.age]: limitRange => -limitRange.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
item => item.getName(), item => item.getName(),
@ -51,7 +52,7 @@ export class LimitRanges extends React.Component<LimitRangesProps> {
limitRange.getName(), limitRange.getName(),
<KubeObjectStatusIcon key="icon" object={limitRange}/>, <KubeObjectStatusIcon key="icon" object={limitRange}/>,
limitRange.getNs(), limitRange.getNs(),
limitRange.getAge(), <KubeObjectAge key="age" object={limitRange} />,
]} ]}
/> />
); );

View File

@ -12,6 +12,7 @@ import { configMapsStore } from "./config-maps.store";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { ConfigMapsRouteParams } from "../../../common/routes"; import type { ConfigMapsRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -30,16 +31,17 @@ export class ConfigMaps extends React.Component<ConfigMapsProps> {
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="configuration_configmaps" tableId="configuration_configmaps"
className="ConfigMaps" store={configMapsStore} className="ConfigMaps"
store={configMapsStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: item => item.getName(), [columnId.name]: configMap => configMap.getName(),
[columnId.namespace]: item => item.getNs(), [columnId.namespace]: configMap => configMap.getNs(),
[columnId.keys]: item => item.getKeys(), [columnId.keys]: configMap => configMap.getKeys(),
[columnId.age]: item => item.getTimeDiffFromNow(), [columnId.age]: configMap => -configMap.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
item => item.getSearchFields(), configMap => configMap.getSearchFields(),
item => item.getKeys(), configMap => configMap.getKeys(),
]} ]}
renderHeaderTitle="Config Maps" renderHeaderTitle="Config Maps"
renderTableHeader={[ renderTableHeader={[
@ -54,7 +56,7 @@ export class ConfigMaps extends React.Component<ConfigMapsProps> {
<KubeObjectStatusIcon key="icon" object={configMap}/>, <KubeObjectStatusIcon key="icon" object={configMap}/>,
configMap.getNs(), configMap.getNs(),
configMap.getKeys().join(", "), configMap.getKeys().join(", "),
configMap.getAge(), <KubeObjectAge key="age" object={configMap} />,
]} ]}
/> />
); );

View File

@ -12,6 +12,7 @@ import type { PodDisruptionBudget } from "../../../common/k8s-api/endpoints/podd
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { KubeObjectDetailsProps } from "../kube-object-details"; import type { KubeObjectDetailsProps } from "../kube-object-details";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -42,7 +43,7 @@ export class PodDisruptionBudgets extends React.Component<PodDisruptionBudgetsPr
[columnId.maxUnavailable]: pdb => pdb.getMaxUnavailable(), [columnId.maxUnavailable]: pdb => pdb.getMaxUnavailable(),
[columnId.currentHealthy]: pdb => pdb.getCurrentHealthy(), [columnId.currentHealthy]: pdb => pdb.getCurrentHealthy(),
[columnId.desiredHealthy]: pdb => pdb.getDesiredHealthy(), [columnId.desiredHealthy]: pdb => pdb.getDesiredHealthy(),
[columnId.age]: pdb => pdb.getAge(), [columnId.age]: pdb => -pdb.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
pdb => pdb.getSearchFields(), pdb => pdb.getSearchFields(),
@ -58,18 +59,16 @@ export class PodDisruptionBudgets extends React.Component<PodDisruptionBudgetsPr
{ title: "Desired Healthy", className: "desired-healthy", sortBy: columnId.desiredHealthy, id: columnId.desiredHealthy }, { title: "Desired Healthy", className: "desired-healthy", sortBy: columnId.desiredHealthy, id: columnId.desiredHealthy },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]} ]}
renderTableContents={pdb => { renderTableContents={pdb => [
return [ pdb.getName(),
pdb.getName(), <KubeObjectStatusIcon key="icon" object={pdb} />,
<KubeObjectStatusIcon key="icon" object={pdb} />, pdb.getNs(),
pdb.getNs(), pdb.getMinAvailable(),
pdb.getMinAvailable(), pdb.getMaxUnavailable(),
pdb.getMaxUnavailable(), pdb.getCurrentHealthy(),
pdb.getCurrentHealthy(), pdb.getDesiredHealthy(),
pdb.getDesiredHealthy(), <KubeObjectAge key="age" object={pdb} />,
pdb.getAge(), ]}
];
}}
/> />
); );
} }

View File

@ -13,6 +13,7 @@ import { AddQuotaDialog } from "./add-quota-dialog";
import { resourceQuotaStore } from "./resource-quotas.store"; import { resourceQuotaStore } from "./resource-quotas.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { ResourceQuotaRouteParams } from "../../../common/routes"; import type { ResourceQuotaRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -31,15 +32,16 @@ export class ResourceQuotas extends React.Component<ResourceQuotasProps> {
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="configuration_quotas" tableId="configuration_quotas"
className="ResourceQuotas" store={resourceQuotaStore} className="ResourceQuotas"
store={resourceQuotaStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: item => item.getName(), [columnId.name]: resourceQuota => resourceQuota.getName(),
[columnId.namespace]: item => item.getNs(), [columnId.namespace]: resourceQuota => resourceQuota.getNs(),
[columnId.age]: item => item.getTimeDiffFromNow(), [columnId.age]: resourceQuota => -resourceQuota.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
item => item.getSearchFields(), resourceQuota => resourceQuota.getSearchFields(),
item => item.getName(), resourceQuota => resourceQuota.getName(),
]} ]}
renderHeaderTitle="Resource Quotas" renderHeaderTitle="Resource Quotas"
renderTableHeader={[ renderTableHeader={[
@ -52,7 +54,7 @@ export class ResourceQuotas extends React.Component<ResourceQuotasProps> {
resourceQuota.getName(), resourceQuota.getName(),
<KubeObjectStatusIcon key="icon" object={resourceQuota}/>, <KubeObjectStatusIcon key="icon" object={resourceQuota}/>,
resourceQuota.getNs(), resourceQuota.getNs(),
resourceQuota.getAge(), <KubeObjectAge key="age" object={resourceQuota} />,
]} ]}
addRemoveButtons={{ addRemoveButtons={{
onAdd: () => AddQuotaDialog.open(), onAdd: () => AddQuotaDialog.open(),

View File

@ -14,6 +14,7 @@ import { Badge } from "../badge";
import { secretsStore } from "./secrets.store"; import { secretsStore } from "./secrets.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { SecretsRouteParams } from "../../../common/routes"; import type { SecretsRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -35,18 +36,19 @@ export class Secrets extends React.Component<SecretsProps> {
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="configuration_secrets" tableId="configuration_secrets"
className="Secrets" store={secretsStore} className="Secrets"
store={secretsStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: item => item.getName(), [columnId.name]: secret => secret.getName(),
[columnId.namespace]: item => item.getNs(), [columnId.namespace]: secret => secret.getNs(),
[columnId.labels]: item => item.getLabels(), [columnId.labels]: secret => secret.getLabels(),
[columnId.keys]: item => item.getKeys(), [columnId.keys]: secret => secret.getKeys(),
[columnId.type]: item => item.type, [columnId.type]: secret => secret.type,
[columnId.age]: item => item.getTimeDiffFromNow(), [columnId.age]: secret => -secret.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
item => item.getSearchFields(), secret => secret.getSearchFields(),
item => item.getKeys(), secret => secret.getKeys(),
]} ]}
renderHeaderTitle="Secrets" renderHeaderTitle="Secrets"
renderTableHeader={[ renderTableHeader={[
@ -65,7 +67,7 @@ export class Secrets extends React.Component<SecretsProps> {
secret.getLabels().map(label => <Badge scrollable key={label} label={label} expandable={false}/>), secret.getLabels().map(label => <Badge scrollable key={label} label={label} expandable={false}/>),
secret.getKeys().join(", "), secret.getKeys().join(", "),
secret.type, secret.type,
secret.getAge(), <KubeObjectAge key="age" object={secret} />,
]} ]}
addRemoveButtons={{ addRemoveButtons={{
onAdd: () => AddSecretDialog.open(), onAdd: () => AddSecretDialog.open(),

View File

@ -12,11 +12,10 @@ import { Link } from "react-router-dom";
import { stopPropagation } from "../../utils"; import { stopPropagation } from "../../utils";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import { crdStore } from "./crd.store"; import { crdStore } from "./crd.store";
import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/crd.api";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { createPageParam } from "../../navigation"; import { createPageParam } from "../../navigation";
import { Icon } from "../icon"; import { Icon } from "../icon";
import type { TableSortCallbacks } from "../table"; import { KubeObjectAge } from "../kube-object/age";
export const crdGroupsUrlParam = createPageParam<string[]>({ export const crdGroupsUrlParam = createPageParam<string[]>({
name: "groups", name: "groups",
@ -63,12 +62,6 @@ export class CustomResourceDefinitions extends React.Component {
render() { render() {
const { items, selectedGroups } = this; const { items, selectedGroups } = this;
const sortingCallbacks: TableSortCallbacks<CustomResourceDefinition> = {
[columnId.kind]: crd => crd.getResourceKind(),
[columnId.group]: crd => crd.getGroup(),
[columnId.version]: crd => crd.getVersion(),
[columnId.scope]: crd => crd.getScope(),
};
return ( return (
<KubeObjectListLayout <KubeObjectListLayout
@ -79,8 +72,20 @@ export class CustomResourceDefinitions extends React.Component {
// Don't subscribe the `crdStore` because <Sidebar> already has and is always mounted // Don't subscribe the `crdStore` because <Sidebar> already has and is always mounted
subscribeStores={false} subscribeStores={false}
items={items} items={items}
sortingCallbacks={sortingCallbacks} sortingCallbacks={{
searchFilters={Object.values(sortingCallbacks)} [columnId.kind]: crd => crd.getResourceKind(),
[columnId.group]: crd => crd.getGroup(),
[columnId.version]: crd => crd.getVersion(),
[columnId.scope]: crd => crd.getScope(),
[columnId.age]: crd => -crd.getCreationTimestamp(),
}}
searchFilters={[
crd => crd.getResourceKind(),
crd => crd.getGroup(),
crd => crd.getVersion(),
crd => crd.getScope(),
crd => -crd.getCreationTimestamp(),
]}
renderHeaderTitle="Custom Resources" renderHeaderTitle="Custom Resources"
customizeHeader={({ filters, ...headerPlaceholders }) => { customizeHeader={({ filters, ...headerPlaceholders }) => {
let placeholder = <>All groups</>; let placeholder = <>All groups</>;
@ -131,7 +136,7 @@ export class CustomResourceDefinitions extends React.Component {
crd.getGroup(), crd.getGroup(),
crd.getVersion(), crd.getVersion(),
crd.getScope(), crd.getScope(),
crd.getAge(), <KubeObjectAge key="age" object={crd} />,
]} ]}
/> />
); );

View File

@ -6,17 +6,16 @@
import "./crd-resources.scss"; import "./crd-resources.scss";
import React from "react"; import React from "react";
import jsonPath from "jsonpath"; import { value } from "jsonpath";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router"; import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { computed, makeObservable } from "mobx"; import { computed, makeObservable } from "mobx";
import { crdStore } from "./crd.store"; import { crdStore } from "./crd.store";
import type { TableSortCallbacks } from "../table";
import { apiManager } from "../../../common/k8s-api/api-manager"; import { apiManager } from "../../../common/k8s-api/api-manager";
import { parseJsonPath } from "../../utils/jsonPath"; import { parseJsonPath } from "../../utils/jsonPath";
import type { CRDRouteParams } from "../../../common/routes"; import type { CRDRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
export interface CustomResourceDefinitionResourcesProps extends RouteComponentProps<CRDRouteParams> { export interface CustomResourceDefinitionResourcesProps extends RouteComponentProps<CRDRouteParams> {
} }
@ -47,29 +46,13 @@ export class CustomResourceDefinitionResources extends React.Component<CustomRes
render() { render() {
const { crd, store } = this; const { crd, store } = this;
if (!crd) return null; if (!crd) {
return null;
}
const isNamespaced = crd.isNamespaced(); const isNamespaced = crd.isNamespaced();
const extraColumns = crd.getPrinterColumns(false); // Cols with priority bigger than 0 are shown in details const extraColumns = crd.getPrinterColumns(false); // Cols with priority bigger than 0 are shown in details
const sortingCallbacks: TableSortCallbacks<KubeObject> = {
[columnId.name]: item => item.getName(),
[columnId.namespace]: item => item.getNs(),
[columnId.age]: item => item.getTimeDiffFromNow(),
};
extraColumns.forEach(column => {
sortingCallbacks[column.name] = item => jsonPath.value(item, parseJsonPath(column.jsonPath.slice(1)));
});
const version = crd.getPreferedVersion(); const version = crd.getPreferedVersion();
const loadFailedPrefix = <p>Failed to load {crd.getPluralName()}</p>;
const failedToLoadMessage = version.served
? loadFailedPrefix
: (
<>
{loadFailedPrefix}
<p>Prefered version ({crd.getGroup()}/{version.name}) is not served</p>
</>
);
return ( return (
<KubeObjectListLayout <KubeObjectListLayout
@ -78,9 +61,17 @@ export class CustomResourceDefinitionResources extends React.Component<CustomRes
tableId="crd_resources" tableId="crd_resources"
className="CrdResources" className="CrdResources"
store={store} store={store}
sortingCallbacks={sortingCallbacks} sortingCallbacks={{
[columnId.name]: customResource => customResource.getName(),
[columnId.namespace]: customResource => customResource.getNs(),
[columnId.age]: customResource => -customResource.getCreationTimestamp(),
...Object.fromEntries(extraColumns.map(({ name, jsonPath }) => [
name,
customResource => value(customResource, parseJsonPath(jsonPath.slice(1))),
])),
}}
searchFilters={[ searchFilters={[
item => item.getSearchFields(), customResource => customResource.getSearchFields(),
]} ]}
renderHeaderTitle={crd.getResourceKind()} renderHeaderTitle={crd.getResourceKind()}
customizeHeader={({ searchProps, ...headerPlaceholders }) => ({ customizeHeader={({ searchProps, ...headerPlaceholders }) => ({
@ -93,36 +84,39 @@ export class CustomResourceDefinitionResources extends React.Component<CustomRes
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))); let rawValue = value(crdInstance, parseJsonPath(column.jsonPath.slice(1)));
if (Array.isArray(value) || typeof value === "object") { if (Array.isArray(rawValue) || typeof rawValue === "object") {
value = JSON.stringify(value); rawValue = JSON.stringify(rawValue);
} }
return { return {
renderBoolean: true, renderBoolean: true,
children: value, children: rawValue,
}; };
}), }),
crdInstance.getAge(), <KubeObjectAge key="age" object={crdInstance} />,
]} ]}
failedToLoadMessage={failedToLoadMessage} failedToLoadMessage={(
<>
<p>Failed to load {crd.getPluralName()}</p>
{!version.served && (
<p>Prefered version ({crd.getGroup()}/{version.name}) is not served</p>
)}
</>
)}
/> />
); );
} }

View File

@ -18,6 +18,7 @@ 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 { ReactiveDuration } from "../duration/reactive-duration";
export interface EventDetailsProps extends KubeObjectDetailsProps<KubeEvent> { export interface EventDetailsProps extends KubeObjectDetailsProps<KubeEvent> {
} }
@ -54,10 +55,14 @@ export class EventDetails extends React.Component<EventDetailsProps> {
{event.getSource()} {event.getSource()}
</DrawerItem> </DrawerItem>
<DrawerItem name="First seen"> <DrawerItem name="First seen">
{event.getFirstSeenTime()} ago (<LocaleDate date={event.firstTimestamp} />) <ReactiveDuration timestamp={event.firstTimestamp} />
{" ago "}
(<LocaleDate date={event.firstTimestamp} />)
</DrawerItem> </DrawerItem>
<DrawerItem name="Last seen"> <DrawerItem name="Last seen">
{event.getLastSeenTime()} ago (<LocaleDate date={event.lastTimestamp} />) <ReactiveDuration timestamp={event.lastTimestamp} />
{" ago "}
(<LocaleDate date={event.lastTimestamp} />)
</DrawerItem> </DrawerItem>
<DrawerItem name="Count"> <DrawerItem name="Count">
{count} {count}

View File

@ -29,7 +29,7 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
protected sortItems(items: KubeEvent[]) { protected sortItems(items: KubeEvent[]) {
return super.sortItems(items, [ return super.sortItems(items, [
event => event.getTimeDiffFromNow(), // keep events order as timeline ("fresh" on top) event => -event.getCreationTimestamp(), // keep events order as timeline ("fresh" on top)
], "asc"); ], "asc");
} }

View File

@ -22,6 +22,8 @@ import { Icon } from "../icon";
import { eventsURL } from "../../../common/routes"; import { eventsURL } from "../../../common/routes";
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 { KubeObjectAge } from "../kube-object/age";
import { ReactiveDuration } from "../duration/reactive-duration";
enum columnId { enum columnId {
message = "message", message = "message",
@ -47,7 +49,6 @@ const defaultProps: Partial<EventsProps> = {
@observer @observer
export class Events extends React.Component<EventsProps> { export class Events extends React.Component<EventsProps> {
static defaultProps = defaultProps as object; static defaultProps = defaultProps as object;
now = Date.now();
@observable sorting: TableSortParams = { @observable sorting: TableSortParams = {
sortBy: columnId.age, sortBy: columnId.age,
@ -59,8 +60,8 @@ export class Events extends React.Component<EventsProps> {
[columnId.type]: event => event.type, [columnId.type]: event => event.type,
[columnId.object]: event => event.involvedObject.name, [columnId.object]: event => event.involvedObject.name,
[columnId.count]: event => event.count, [columnId.count]: event => event.count,
[columnId.age]: event => event.getTimeDiffFromNow(), [columnId.age]: event => -event.getCreationTimestamp(),
[columnId.lastSeen]: event => this.now - new Date(event.lastTimestamp).getTime(), [columnId.lastSeen]: event => -new Date(event.lastTimestamp).getTime(),
}; };
constructor(props: EventsProps) { constructor(props: EventsProps) {
@ -185,8 +186,8 @@ export class Events extends React.Component<EventsProps> {
</Link>, </Link>,
event.getSource(), event.getSource(),
event.count, event.count,
event.getAge(), <KubeObjectAge key="age" object={event} />,
event.getLastSeenTime(), <ReactiveDuration key="last-seen" timestamp={event.lastTimestamp} />,
]; ];
}} }}
/> />

View File

@ -51,44 +51,34 @@ class NonInjectedKubeEventDetails extends React.Component<KubeEventDetailsProps
const events = eventStore.getEventsByObject(object); const events = eventStore.getEventsByObject(object);
if (!events.length) {
return (
<DrawerTitle className="flex gaps align-center">
<span>Events</span>
</DrawerTitle>
);
}
return ( return (
<div> <div>
<DrawerTitle className="flex gaps align-center"> <DrawerTitle className="flex gaps align-center">
<span>Events</span> <span>Events</span>
</DrawerTitle> </DrawerTitle>
<div className="KubeEventDetails"> {events.length > 0 && (
{events.map(evt => { <div className="KubeEventDetails">
const { message, count, lastTimestamp, involvedObject } = evt; {events.map(event => (
<div className="event" key={event.getId()}>
return ( <div className={cssNames("title", { warning: event.isWarning() })}>
<div className="event" key={evt.getId()}> {event.message}
<div className={cssNames("title", { warning: evt.isWarning() })}>
{message}
</div> </div>
<DrawerItem name="Source"> <DrawerItem name="Source">
{evt.getSource()} {event.getSource()}
</DrawerItem> </DrawerItem>
<DrawerItem name="Count"> <DrawerItem name="Count">
{count} {event.count}
</DrawerItem> </DrawerItem>
<DrawerItem name="Sub-object"> <DrawerItem name="Sub-object">
{involvedObject.fieldPath} {event.involvedObject.fieldPath}
</DrawerItem> </DrawerItem>
<DrawerItem name="Last seen"> <DrawerItem name="Last seen">
<LocaleDate date={lastTimestamp} /> <LocaleDate date={event.lastTimestamp} />
</DrawerItem> </DrawerItem>
</div> </div>
); ))}
})} </div>
</div> )}
</div> </div>
); );
} }

View File

@ -11,6 +11,7 @@ import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { eventStore } from "./event.store"; import { eventStore } from "./event.store";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import type { KubeEvent } from "../../../common/k8s-api/endpoints/events.api"; import type { KubeEvent } from "../../../common/k8s-api/endpoints/events.api";
import { KubeObjectAge } from "../kube-object/age";
export interface KubeEventIconProps { export interface KubeEventIconProps {
object: KubeObject; object: KubeObject;
@ -48,7 +49,7 @@ export class KubeEventIcon extends React.Component<KubeEventIconProps> {
<div className="msg">{event.message}</div> <div className="msg">{event.message}</div>
<div className="age"> <div className="age">
<Icon material="access_time"/> <Icon material="access_time"/>
{event.getAge(undefined, undefined, true)} <KubeObjectAge object={event} />
</div> </div>
</div> </div>
), ),

View File

@ -33,6 +33,8 @@ import releaseInjectable from "./release.injectable";
import releaseDetailsInjectable from "./release-details.injectable"; import releaseDetailsInjectable from "./release-details.injectable";
import releaseValuesInjectable from "./release-values.injectable"; import releaseValuesInjectable from "./release-values.injectable";
import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-shown.injectable"; import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-shown.injectable";
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
import { KubeObjectAge } from "../../kube-object/age";
export interface ReleaseDetailsProps { export interface ReleaseDetailsProps {
hideDetails(): void; hideDetails(): void;
@ -150,45 +152,46 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
); );
} }
renderResources() { renderResources(resources: KubeObject[]) {
const { resources } = this.details;
if (!resources) return null;
const groups = groupBy(resources, item => item.kind);
const tables = Object.entries(groups).map(([kind, items]) => {
return (
<React.Fragment key={kind}>
<SubTitle title={kind}/>
<Table scrollable={false}>
<TableHead sticky={false}>
<TableCell className="name">Name</TableCell>
{items[0].getNs() && <TableCell className="namespace">Namespace</TableCell>}
<TableCell className="age">Age</TableCell>
</TableHead>
{items.map(item => {
const name = item.getName();
const namespace = item.getNs();
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == item.apiVersion);
const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : "";
return (
<TableRow key={item.getId()}>
<TableCell className="name">
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
</TableCell>
{namespace && <TableCell className="namespace">{namespace}</TableCell>}
<TableCell className="age">{item.getAge()}</TableCell>
</TableRow>
);
})}
</Table>
</React.Fragment>
);
});
return ( return (
<div className="resources"> <div className="resources">
{tables} {
Object.entries(groupBy(resources, item => item.kind))
.map(([kind, items]) => (
<React.Fragment key={kind}>
<SubTitle title={kind} />
<Table scrollable={false}>
<TableHead sticky={false}>
<TableCell className="name">Name</TableCell>
{items[0].getNs() && <TableCell className="namespace">Namespace</TableCell>}
<TableCell className="age">Age</TableCell>
</TableHead>
{items.map(item => {
const name = item.getName();
const namespace = item.getNs();
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == item.apiVersion);
const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : "";
return (
<TableRow key={item.getId()}>
<TableCell className="name">
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
</TableCell>
{namespace && (
<TableCell className="namespace">
{namespace}
</TableCell>
)}
<TableCell className="age">
<KubeObjectAge key="age" object={item} />
</TableCell>
</TableRow>
);
})}
</Table>
</React.Fragment>
))
}
</div> </div>
); );
} }
@ -200,6 +203,8 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
return <Spinner center/>; return <Spinner center/>;
} }
const { resources } = this.details;
return ( return (
<div> <div>
<DrawerItem name="Chart" className="chart"> <DrawerItem name="Chart" className="chart">
@ -236,7 +241,7 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
<DrawerTitle title="Notes"/> <DrawerTitle title="Notes"/>
{this.renderNotes()} {this.renderNotes()}
<DrawerTitle title="Resources"/> <DrawerTitle title="Resources"/>
{this.renderResources()} {resources && this.renderResources(resources)}
</div> </div>
); );
} }

View File

@ -19,6 +19,7 @@ import { withInjectables } from "@ogre-tools/injectable-react";
import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable"; import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable";
import addNamespaceDialogModelInjectable import addNamespaceDialogModelInjectable
from "./add-namespace-dialog-model/add-namespace-dialog-model.injectable"; from "./add-namespace-dialog-model/add-namespace-dialog-model.injectable";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -43,14 +44,14 @@ export const NonInjectedNamespacesRoute = ({ namespaceStore, openAddNamespaceDia
className="Namespaces" className="Namespaces"
store={namespaceStore} store={namespaceStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: ns => ns.getName(), [columnId.name]: namespace => namespace.getName(),
[columnId.labels]: ns => ns.getLabels(), [columnId.labels]: namespace => namespace.getLabels(),
[columnId.age]: ns => ns.getTimeDiffFromNow(), [columnId.age]: namespace => -namespace.getCreationTimestamp(),
[columnId.status]: ns => ns.getStatus(), [columnId.status]: namespace => namespace.getStatus(),
}} }}
searchFilters={[ searchFilters={[
item => item.getSearchFields(), namespace => namespace.getSearchFields(),
item => item.getStatus(), namespace => namespace.getStatus(),
]} ]}
renderHeaderTitle="Namespaces" renderHeaderTitle="Namespaces"
renderTableHeader={[ renderTableHeader={[
@ -60,12 +61,12 @@ export const NonInjectedNamespacesRoute = ({ namespaceStore, openAddNamespaceDia
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status }, { title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
]} ]}
renderTableContents={item => [ renderTableContents={namespace => [
item.getName(), namespace.getName(),
<KubeObjectStatusIcon key="icon" object={item} />, <KubeObjectStatusIcon key="icon" object={namespace} />,
item.getLabels().map(label => <Badge scrollable key={label} label={label}/>), namespace.getLabels().map(label => <Badge scrollable key={label} label={label}/>),
item.getAge(), <KubeObjectAge key="age" object={namespace} />,
{ title: item.getStatus(), className: item.getStatus().toLowerCase() }, { title: namespace.getStatus(), className: namespace.getStatus().toLowerCase() },
]} ]}
addRemoveButtons={{ addRemoveButtons={{
addTooltip: "Add Namespace", addTooltip: "Add Namespace",

View File

@ -12,6 +12,7 @@ import { endpointStore } from "./endpoints.store";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { EndpointRouteParams } from "../../../common/routes"; import type { EndpointRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -34,7 +35,7 @@ export class Endpoints extends React.Component<EndpointsProps> {
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: endpoint => endpoint.getName(), [columnId.name]: endpoint => endpoint.getName(),
[columnId.namespace]: endpoint => endpoint.getNs(), [columnId.namespace]: endpoint => endpoint.getNs(),
[columnId.age]: endpoint => endpoint.getTimeDiffFromNow(), [columnId.age]: endpoint => -endpoint.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
endpoint => endpoint.getSearchFields(), endpoint => endpoint.getSearchFields(),
@ -52,7 +53,7 @@ export class Endpoints extends React.Component<EndpointsProps> {
<KubeObjectStatusIcon key="icon" object={endpoint} />, <KubeObjectStatusIcon key="icon" object={endpoint} />,
endpoint.getNs(), endpoint.getNs(),
endpoint.toString(), endpoint.toString(),
endpoint.getAge(), <KubeObjectAge key="age" object={endpoint} />,
]} ]}
tableProps={{ tableProps={{
customRowHeights: (item, lineHeight, paddings) => { customRowHeights: (item, lineHeight, paddings) => {

View File

@ -12,6 +12,7 @@ import { ingressStore } from "./ingress.store";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { IngressRouteParams } from "../../../common/routes"; import type { IngressRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -35,7 +36,7 @@ export class Ingresses extends React.Component<IngressesProps> {
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: ingress => ingress.getName(), [columnId.name]: ingress => ingress.getName(),
[columnId.namespace]: ingress => ingress.getNs(), [columnId.namespace]: ingress => ingress.getNs(),
[columnId.age]: ingress => ingress.getTimeDiffFromNow(), [columnId.age]: ingress => -ingress.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
ingress => ingress.getSearchFields(), ingress => ingress.getSearchFields(),
@ -56,7 +57,7 @@ export class Ingresses extends React.Component<IngressesProps> {
ingress.getNs(), ingress.getNs(),
ingress.getLoadBalancers().map(lb => <p key={lb}>{lb}</p>), ingress.getLoadBalancers().map(lb => <p key={lb}>{lb}</p>),
ingress.getRoutes().map(route => <p key={route}>{route}</p>), ingress.getRoutes().map(route => <p key={route}>{route}</p>),
ingress.getAge(), <KubeObjectAge key="age" object={ingress} />,
]} ]}
tableProps={{ tableProps={{
customRowHeights: (item, lineHeight, paddings) => { customRowHeights: (item, lineHeight, paddings) => {

View File

@ -12,6 +12,7 @@ import { KubeObjectListLayout } from "../kube-object-list-layout";
import { networkPolicyStore } from "./network-policy.store"; import { networkPolicyStore } from "./network-policy.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { NetworkPoliciesRouteParams } from "../../../common/routes"; import type { NetworkPoliciesRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -30,14 +31,15 @@ export class NetworkPolicies extends React.Component<NetworkPoliciesProps> {
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="network_policies" tableId="network_policies"
className="NetworkPolicies" store={networkPolicyStore} className="NetworkPolicies"
store={networkPolicyStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: item => item.getName(), [columnId.name]: networkPolicy => networkPolicy.getName(),
[columnId.namespace]: item => item.getNs(), [columnId.namespace]: networkPolicy => networkPolicy.getNs(),
[columnId.age]: item => item.getTimeDiffFromNow(), [columnId.age]: networkPolicy => -networkPolicy.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
item => item.getSearchFields(), networkPolicy => networkPolicy.getSearchFields(),
]} ]}
renderHeaderTitle="Network Policies" renderHeaderTitle="Network Policies"
renderTableHeader={[ renderTableHeader={[
@ -47,12 +49,12 @@ export class NetworkPolicies extends React.Component<NetworkPoliciesProps> {
{ title: "Policy Types", className: "type", id: columnId.types }, { title: "Policy Types", className: "type", id: columnId.types },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]} ]}
renderTableContents={item => [ renderTableContents={networkPolicy => [
item.getName(), networkPolicy.getName(),
<KubeObjectStatusIcon key="icon" object={item} />, <KubeObjectStatusIcon key="icon" object={networkPolicy} />,
item.getNs(), networkPolicy.getNs(),
item.getTypes().join(", "), networkPolicy.getTypes().join(", "),
item.getAge(), <KubeObjectAge key="age" object={networkPolicy} />,
]} ]}
/> />
); );

View File

@ -13,6 +13,7 @@ import { Badge } from "../badge";
import { serviceStore } from "./services.store"; import { serviceStore } from "./services.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { ServicesRouteParams } from "../../../common/routes"; import type { ServicesRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -44,7 +45,7 @@ export class Services extends React.Component<ServicesProps> {
[columnId.ports]: service => (service.spec.ports || []).map(({ port }) => port)[0], [columnId.ports]: service => (service.spec.ports || []).map(({ port }) => port)[0],
[columnId.clusterIp]: service => service.getClusterIp(), [columnId.clusterIp]: service => service.getClusterIp(),
[columnId.type]: service => service.getType(), [columnId.type]: service => service.getType(),
[columnId.age]: service => service.getTimeDiffFromNow(), [columnId.age]: service => -service.getCreationTimestamp(),
[columnId.status]: service => service.getStatus(), [columnId.status]: service => service.getStatus(),
}} }}
searchFilters={[ searchFilters={[
@ -81,7 +82,7 @@ export class Services extends React.Component<ServicesProps> {
service.getPorts().join(", "), service.getPorts().join(", "),
externalIps.join(", ") || "-", externalIps.join(", ") || "-",
service.getSelector().map(label => <Badge key={label} label={label} />), service.getSelector().map(label => <Badge key={label} label={label} />),
service.getAge(), <KubeObjectAge key="age" object={service} />,
{ title: service.getStatus(), className: service.getStatus().toLowerCase() }, { title: service.getStatus(), className: service.getStatus().toLowerCase() },
]; ];
}} }}

View File

@ -23,6 +23,7 @@ import { eventStore } from "../+events/event.store";
import type { NodesRouteParams } from "../../../common/routes"; import type { NodesRouteParams } from "../../../common/routes";
import { makeObservable, observable } from "mobx"; import { makeObservable, observable } from "mobx";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -191,7 +192,7 @@ export class NodesRoute extends React.Component<NodesRouteProps> {
[columnId.conditions]: node => node.getNodeConditionText(), [columnId.conditions]: node => node.getNodeConditionText(),
[columnId.taints]: node => node.getTaints().length, [columnId.taints]: node => node.getTaints().length,
[columnId.roles]: node => node.getRoleLabels(), [columnId.roles]: node => node.getRoleLabels(),
[columnId.age]: node => node.getTimeDiffFromNow(), [columnId.age]: node => -node.getCreationTimestamp(),
[columnId.version]: node => node.getKubeletVersion(), [columnId.version]: node => node.getKubeletVersion(),
}} }}
searchFilters={[ searchFilters={[
@ -231,7 +232,7 @@ export class NodesRoute extends React.Component<NodesRouteProps> {
</>, </>,
node.getRoleLabels(), node.getRoleLabels(),
node.status.nodeInfo.kubeletVersion, node.status.nodeInfo.kubeletVersion,
node.getAge(), <KubeObjectAge key="age" object={node} />,
this.renderConditions(node), this.renderConditions(node),
]; ];
}} }}

View File

@ -10,6 +10,7 @@ import { observer } from "mobx-react";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import { podSecurityPoliciesStore } from "./pod-security-policies.store"; import { podSecurityPoliciesStore } from "./pod-security-policies.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -28,15 +29,15 @@ export class PodSecurityPolicies extends React.Component {
className="PodSecurityPolicies" className="PodSecurityPolicies"
store={podSecurityPoliciesStore} store={podSecurityPoliciesStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: item => item.getName(), [columnId.name]: podSecurityPolicy => podSecurityPolicy.getName(),
[columnId.volumes]: item => item.getVolumes(), [columnId.volumes]: podSecurityPolicy => podSecurityPolicy.getVolumes(),
[columnId.privileged]: item => +item.isPrivileged(), [columnId.privileged]: podSecurityPolicy => +podSecurityPolicy.isPrivileged(),
[columnId.age]: item => item.getTimeDiffFromNow(), [columnId.age]: podSecurityPolicy => -podSecurityPolicy.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
item => item.getSearchFields(), podSecurityPolicy => podSecurityPolicy.getSearchFields(),
item => item.getVolumes(), podSecurityPolicy => podSecurityPolicy.getVolumes(),
item => Object.values(item.getRules()), podSecurityPolicy => Object.values(podSecurityPolicy.getRules()),
]} ]}
renderHeaderTitle="Pod Security Policies" renderHeaderTitle="Pod Security Policies"
renderTableHeader={[ renderTableHeader={[
@ -46,15 +47,13 @@ export class PodSecurityPolicies extends React.Component {
{ title: "Volumes", className: "volumes", sortBy: columnId.volumes, id: columnId.volumes }, { title: "Volumes", className: "volumes", sortBy: columnId.volumes, id: columnId.volumes },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]} ]}
renderTableContents={item => { renderTableContents={podSecurityPolicy => [
return [ podSecurityPolicy.getName(),
item.getName(), <KubeObjectStatusIcon key="icon" object={podSecurityPolicy} />,
<KubeObjectStatusIcon key="icon" object={item} />, podSecurityPolicy.isPrivileged() ? "Yes" : "No",
item.isPrivileged() ? "Yes" : "No", podSecurityPolicy.getVolumes().join(", "),
item.getVolumes().join(", "), <KubeObjectAge key="age" object={podSecurityPolicy} />,
item.getAge(), ]}
];
}}
/> />
); );
} }

View File

@ -12,6 +12,7 @@ import { KubeObjectListLayout } from "../kube-object-list-layout";
import { storageClassStore } from "./storage-class.store"; import { storageClassStore } from "./storage-class.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { StorageClassesRouteParams } from "../../../common/routes"; import type { StorageClassesRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -34,14 +35,14 @@ export class StorageClasses extends React.Component<StorageClassesProps> {
className="StorageClasses" className="StorageClasses"
store={storageClassStore} store={storageClassStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: item => item.getName(), [columnId.name]: storageClass => storageClass.getName(),
[columnId.age]: item => item.getTimeDiffFromNow(), [columnId.age]: storageClass => -storageClass.getCreationTimestamp(),
[columnId.provisioner]: item => item.provisioner, [columnId.provisioner]: storageClass => storageClass.provisioner,
[columnId.reclaimPolicy]: item => item.reclaimPolicy, [columnId.reclaimPolicy]: storageClass => storageClass.reclaimPolicy,
}} }}
searchFilters={[ searchFilters={[
item => item.getSearchFields(), storageClass => storageClass.getSearchFields(),
item => item.provisioner, storageClass => storageClass.provisioner,
]} ]}
renderHeaderTitle="Storage Classes" renderHeaderTitle="Storage Classes"
renderTableHeader={[ renderTableHeader={[
@ -58,7 +59,7 @@ export class StorageClasses extends React.Component<StorageClassesProps> {
storageClass.provisioner, storageClass.provisioner,
storageClass.getReclaimPolicy(), storageClass.getReclaimPolicy(),
storageClass.isDefault() ? "Yes" : null, storageClass.isDefault() ? "Yes" : null,
storageClass.getAge(), <KubeObjectAge key="age" object={storageClass} />,
]} ]}
/> />
); );

View File

@ -17,6 +17,7 @@ import { storageClassApi } from "../../../common/k8s-api/endpoints";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { VolumeClaimsRouteParams } from "../../../common/routes"; import type { VolumeClaimsRouteParams } from "../../../common/routes";
import { getDetailsUrl } from "../kube-detail-params"; import { getDetailsUrl } from "../kube-detail-params";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -48,11 +49,11 @@ export class PersistentVolumeClaims extends React.Component<PersistentVolumeClai
[columnId.status]: pvc => pvc.getStatus(), [columnId.status]: pvc => pvc.getStatus(),
[columnId.size]: pvc => unitsToBytes(pvc.getStorage()), [columnId.size]: pvc => unitsToBytes(pvc.getStorage()),
[columnId.storageClass]: pvc => pvc.spec.storageClassName, [columnId.storageClass]: pvc => pvc.spec.storageClassName,
[columnId.age]: pvc => pvc.getTimeDiffFromNow(), [columnId.age]: pvc => -pvc.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
item => item.getSearchFields(), pvc => pvc.getSearchFields(),
item => item.getPods(podsStore.items).map(pod => pod.getName()), pvc => pvc.getPods(podsStore.items).map(pod => pod.getName()),
]} ]}
renderHeaderTitle="Persistent Volume Claims" renderHeaderTitle="Persistent Volume Claims"
renderTableHeader={[ renderTableHeader={[
@ -85,7 +86,7 @@ export class PersistentVolumeClaims extends React.Component<PersistentVolumeClai
{pod.getName()} {pod.getName()}
</Link> </Link>
)), )),
pvc.getAge(), <KubeObjectAge key="age" object={pvc} />,
{ title: pvc.getStatus(), className: pvc.getStatus().toLowerCase() }, { title: pvc.getStatus(), className: pvc.getStatus().toLowerCase() },
]; ];
}} }}

View File

@ -15,6 +15,7 @@ import { volumesStore } from "./volumes.store";
import { pvcApi, storageClassApi } from "../../../common/k8s-api/endpoints"; import { pvcApi, storageClassApi } from "../../../common/k8s-api/endpoints";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { VolumesRouteParams } from "../../../common/routes"; import type { VolumesRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -38,15 +39,15 @@ export class PersistentVolumes extends React.Component<PersistentVolumesProps> {
className="PersistentVolumes" className="PersistentVolumes"
store={volumesStore} store={volumesStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: item => item.getName(), [columnId.name]: volume => volume.getName(),
[columnId.storageClass]: item => item.getStorageClass(), [columnId.storageClass]: volume => volume.getStorageClass(),
[columnId.capacity]: item => item.getCapacity(true), [columnId.capacity]: volume => volume.getCapacity(true),
[columnId.status]: item => item.getStatus(), [columnId.status]: volume => volume.getStatus(),
[columnId.age]: item => item.getTimeDiffFromNow(), [columnId.age]: volume => -volume.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
item => item.getSearchFields(), volume => volume.getSearchFields(),
item => item.getClaimRefName(), volume => volume.getClaimRefName(),
]} ]}
renderHeaderTitle="Persistent Volumes" renderHeaderTitle="Persistent Volumes"
renderTableHeader={[ renderTableHeader={[
@ -76,7 +77,7 @@ export class PersistentVolumes extends React.Component<PersistentVolumesProps> {
{claimRef.name} {claimRef.name}
</Link> </Link>
), ),
volume.getAge(), <KubeObjectAge key="age" object={volume} />,
{ title: volume.getStatus(), className: volume.getStatus().toLowerCase() }, { title: volume.getStatus(), className: volume.getStatus().toLowerCase() },
]; ];
}} }}

View File

@ -15,6 +15,7 @@ import { clusterRoleBindingsStore } from "./store";
import { clusterRolesStore } from "../+cluster-roles/store"; import { clusterRolesStore } from "../+cluster-roles/store";
import { serviceAccountsStore } from "../+service-accounts/store"; import { serviceAccountsStore } from "../+service-accounts/store";
import type { ClusterRoleBindingsRouteParams } from "../../../../common/routes"; import type { ClusterRoleBindingsRouteParams } from "../../../../common/routes";
import { KubeObjectAge } from "../../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -40,7 +41,7 @@ export class ClusterRoleBindings extends React.Component<ClusterRoleBindingsProp
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: binding => binding.getName(), [columnId.name]: binding => binding.getName(),
[columnId.bindings]: binding => binding.getSubjectNames(), [columnId.bindings]: binding => binding.getSubjectNames(),
[columnId.age]: binding => binding.getTimeDiffFromNow(), [columnId.age]: binding => -binding.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
binding => binding.getSearchFields(), binding => binding.getSearchFields(),
@ -57,7 +58,7 @@ export class ClusterRoleBindings extends React.Component<ClusterRoleBindingsProp
binding.getName(), binding.getName(),
<KubeObjectStatusIcon key="icon" object={binding} />, <KubeObjectStatusIcon key="icon" object={binding} />,
binding.getSubjectNames(), binding.getSubjectNames(),
binding.getAge(), <KubeObjectAge key="age" object={binding} />,
]} ]}
addRemoveButtons={{ addRemoveButtons={{
onAdd: () => ClusterRoleBindingDialog.open(), onAdd: () => ClusterRoleBindingDialog.open(),

View File

@ -13,6 +13,7 @@ import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { AddClusterRoleDialog } from "./add-dialog"; import { AddClusterRoleDialog } from "./add-dialog";
import { clusterRolesStore } from "./store"; import { clusterRolesStore } from "./store";
import type { ClusterRolesRouteParams } from "../../../../common/routes"; import type { ClusterRolesRouteParams } from "../../../../common/routes";
import { KubeObjectAge } from "../../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -35,7 +36,7 @@ export class ClusterRoles extends React.Component<ClusterRolesProps> {
store={clusterRolesStore} store={clusterRolesStore}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: clusterRole => clusterRole.getName(), [columnId.name]: clusterRole => clusterRole.getName(),
[columnId.age]: clusterRole => clusterRole.getTimeDiffFromNow(), [columnId.age]: clusterRole => -clusterRole.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
clusterRole => clusterRole.getSearchFields(), clusterRole => clusterRole.getSearchFields(),
@ -49,7 +50,7 @@ export class ClusterRoles extends React.Component<ClusterRolesProps> {
renderTableContents={clusterRole => [ renderTableContents={clusterRole => [
clusterRole.getName(), clusterRole.getName(),
<KubeObjectStatusIcon key="icon" object={clusterRole} />, <KubeObjectStatusIcon key="icon" object={clusterRole} />,
clusterRole.getAge(), <KubeObjectAge key="age" object={clusterRole} />,
]} ]}
addRemoveButtons={{ addRemoveButtons={{
onAdd: () => AddClusterRoleDialog.open(), onAdd: () => AddClusterRoleDialog.open(),

View File

@ -15,6 +15,7 @@ import { rolesStore } from "../+roles/store";
import { clusterRolesStore } from "../+cluster-roles/store"; import { clusterRolesStore } from "../+cluster-roles/store";
import { serviceAccountsStore } from "../+service-accounts/store"; import { serviceAccountsStore } from "../+service-accounts/store";
import type { RoleBindingsRouteParams } from "../../../../common/routes"; import type { RoleBindingsRouteParams } from "../../../../common/routes";
import { KubeObjectAge } from "../../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -41,7 +42,7 @@ export class RoleBindings extends React.Component<RoleBindingsProps> {
[columnId.name]: binding => binding.getName(), [columnId.name]: binding => binding.getName(),
[columnId.namespace]: binding => binding.getNs(), [columnId.namespace]: binding => binding.getNs(),
[columnId.bindings]: binding => binding.getSubjectNames(), [columnId.bindings]: binding => binding.getSubjectNames(),
[columnId.age]: binding => binding.getTimeDiffFromNow(), [columnId.age]: binding => -binding.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
binding => binding.getSearchFields(), binding => binding.getSearchFields(),
@ -60,7 +61,7 @@ export class RoleBindings extends React.Component<RoleBindingsProps> {
<KubeObjectStatusIcon key="icon" object={binding} />, <KubeObjectStatusIcon key="icon" object={binding} />,
binding.getNs(), binding.getNs(),
binding.getSubjectNames(), binding.getSubjectNames(),
binding.getAge(), <KubeObjectAge key="age" object={binding} />,
]} ]}
addRemoveButtons={{ addRemoveButtons={{
onAdd: () => RoleBindingDialog.open(), onAdd: () => RoleBindingDialog.open(),

View File

@ -13,6 +13,7 @@ import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { AddRoleDialog } from "./add-dialog"; import { AddRoleDialog } from "./add-dialog";
import { rolesStore } from "./store"; import { rolesStore } from "./store";
import type { RolesRouteParams } from "../../../../common/routes"; import type { RolesRouteParams } from "../../../../common/routes";
import { KubeObjectAge } from "../../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -36,7 +37,7 @@ export class Roles extends React.Component<RolesProps> {
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: role => role.getName(), [columnId.name]: role => role.getName(),
[columnId.namespace]: role => role.getNs(), [columnId.namespace]: role => role.getNs(),
[columnId.age]: role => role.getTimeDiffFromNow(), [columnId.age]: role => -role.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
role => role.getSearchFields(), role => role.getSearchFields(),
@ -52,7 +53,7 @@ export class Roles extends React.Component<RolesProps> {
role.getName(), role.getName(),
<KubeObjectStatusIcon key="icon" object={role} />, <KubeObjectStatusIcon key="icon" object={role} />,
role.getNs(), role.getNs(),
role.getAge(), <KubeObjectAge key="age" object={role} />,
]} ]}
addRemoveButtons={{ addRemoveButtons={{
onAdd: () => AddRoleDialog.open(), onAdd: () => AddRoleDialog.open(),

View File

@ -13,6 +13,7 @@ import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { CreateServiceAccountDialog } from "./create-dialog"; import { CreateServiceAccountDialog } from "./create-dialog";
import { serviceAccountsStore } from "./store"; import { serviceAccountsStore } from "./store";
import type { ServiceAccountsRouteParams } from "../../../../common/routes"; import type { ServiceAccountsRouteParams } from "../../../../common/routes";
import { KubeObjectAge } from "../../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -35,7 +36,7 @@ export class ServiceAccounts extends React.Component<ServiceAccountsProps> {
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: account => account.getName(), [columnId.name]: account => account.getName(),
[columnId.namespace]: account => account.getNs(), [columnId.namespace]: account => account.getNs(),
[columnId.age]: account => account.getTimeDiffFromNow(), [columnId.age]: account => -account.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
account => account.getSearchFields(), account => account.getSearchFields(),
@ -51,7 +52,7 @@ export class ServiceAccounts extends React.Component<ServiceAccountsProps> {
account.getName(), account.getName(),
<KubeObjectStatusIcon key="icon" object={account} />, <KubeObjectStatusIcon key="icon" object={account} />,
account.getNs(), account.getNs(),
account.getAge(), <KubeObjectAge key="age" object={account} />,
]} ]}
addRemoveButtons={{ addRemoveButtons={{
onAdd: () => CreateServiceAccountDialog.open(), onAdd: () => CreateServiceAccountDialog.open(),

View File

@ -15,6 +15,7 @@ import { KubeObjectListLayout } from "../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { CronJobsRouteParams } from "../../../common/routes"; import type { CronJobsRouteParams } from "../../../common/routes";
import moment from "moment"; import moment from "moment";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -36,7 +37,8 @@ export class CronJobs extends React.Component<CronJobsProps> {
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="workload_cronjobs" tableId="workload_cronjobs"
className="CronJobs" store={cronJobStore} className="CronJobs"
store={cronJobStore}
dependentStores={[jobStore, eventStore]} dependentStores={[jobStore, eventStore]}
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: cronJob => cronJob.getName(), [columnId.name]: cronJob => cronJob.getName(),
@ -48,7 +50,7 @@ export class CronJobs extends React.Component<CronJobsProps> {
? moment().diff(cronJob.status.lastScheduleTime) ? moment().diff(cronJob.status.lastScheduleTime)
: 0 : 0
), ),
[columnId.age]: cronJob => cronJob.getTimeDiffFromNow(), [columnId.age]: cronJob => -cronJob.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
cronJob => cronJob.getSearchFields(), cronJob => cronJob.getSearchFields(),
@ -73,7 +75,7 @@ export class CronJobs extends React.Component<CronJobsProps> {
cronJob.getSuspendFlag(), cronJob.getSuspendFlag(),
cronJobStore.getActiveJobsNum(cronJob), cronJobStore.getActiveJobsNum(cronJob),
cronJob.getLastScheduleTime(), cronJob.getLastScheduleTime(),
cronJob.getAge(), <KubeObjectAge key="age" object={cronJob} />,
]} ]}
/> />
); );

View File

@ -16,6 +16,7 @@ import { KubeObjectListLayout } from "../kube-object-list-layout";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { DaemonSetsRouteParams } from "../../../common/routes"; import type { DaemonSetsRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -39,13 +40,14 @@ export class DaemonSets extends React.Component<DaemonSetsProps> {
<KubeObjectListLayout <KubeObjectListLayout
isConfigurable isConfigurable
tableId="workload_daemonsets" tableId="workload_daemonsets"
className="DaemonSets" store={daemonSetStore} className="DaemonSets"
store={daemonSetStore}
dependentStores={[podsStore, eventStore]} // status icon component uses event store dependentStores={[podsStore, eventStore]} // status icon component uses event store
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: daemonSet => daemonSet.getName(), [columnId.name]: daemonSet => daemonSet.getName(),
[columnId.namespace]: daemonSet => daemonSet.getNs(), [columnId.namespace]: daemonSet => daemonSet.getNs(),
[columnId.pods]: daemonSet => this.getPodsLength(daemonSet), [columnId.pods]: daemonSet => this.getPodsLength(daemonSet),
[columnId.age]: daemonSet => daemonSet.getTimeDiffFromNow(), [columnId.age]: daemonSet => -daemonSet.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
daemonSet => daemonSet.getSearchFields(), daemonSet => daemonSet.getSearchFields(),
@ -68,7 +70,7 @@ export class DaemonSets extends React.Component<DaemonSetsProps> {
daemonSet.getNodeSelectors().map(selector => ( daemonSet.getNodeSelectors().map(selector => (
<Badge key={selector} label={selector} scrollable/> <Badge key={selector} label={selector} scrollable/>
)), )),
daemonSet.getAge(), <KubeObjectAge key="age" object={daemonSet} />,
]} ]}
/> />
); );

View File

@ -16,6 +16,7 @@ import { Table, TableCell, TableHead, TableRow } from "../table";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; import { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
import { showDetails } from "../kube-detail-params"; import { showDetails } from "../kube-detail-params";
import { KubeObjectAge } from "../kube-object/age";
enum sortBy { enum sortBy {
@ -71,25 +72,23 @@ export class DeploymentReplicaSets extends React.Component<DeploymentReplicaSets
<TableCell className="actions"/> <TableCell className="actions"/>
</TableHead> </TableHead>
{ {
replicaSets.map(replica => { replicaSets.map(replica => (
return ( <TableRow
<TableRow key={replica.getId()}
key={replica.getId()} sortItem={replica}
sortItem={replica} nowrap
nowrap onClick={prevDefault(() => showDetails(replica.selfLink, false))}
onClick={prevDefault(() => showDetails(replica.selfLink, false))} >
> <TableCell className="name">{replica.getName()}</TableCell>
<TableCell className="name">{replica.getName()}</TableCell> <TableCell className="warning"><KubeObjectStatusIcon key="icon" object={replica} /></TableCell>
<TableCell className="warning"><KubeObjectStatusIcon key="icon" object={replica}/></TableCell> <TableCell className="namespace">{replica.getNs()}</TableCell>
<TableCell className="namespace">{replica.getNs()}</TableCell> <TableCell className="pods">{this.getPodsLength(replica)}</TableCell>
<TableCell className="pods">{this.getPodsLength(replica)}</TableCell> <TableCell className="age"><KubeObjectAge key="age" object={replica} /></TableCell>
<TableCell className="age">{replica.getAge()}</TableCell> <TableCell className="actions" onClick={stopPropagation}>
<TableCell className="actions" onClick={stopPropagation}> <ReplicaSetMenu object={replica} />
<ReplicaSetMenu object={replica}/> </TableCell>
</TableCell> </TableRow>
</TableRow> ))
);
})
} }
</Table> </Table>
</div> </div>

View File

@ -87,6 +87,7 @@ const dummyDeployment: Deployment = {
getName: jest.fn(), getName: jest.fn(),
getNs: jest.fn(), getNs: jest.fn(),
getAge: jest.fn(), getAge: jest.fn(),
getCreationTimestamp: jest.fn(),
getTimeDiffFromNow: jest.fn(), getTimeDiffFromNow: jest.fn(),
getFinalizers: jest.fn(), getFinalizers: jest.fn(),
getLabels: jest.fn(), getLabels: jest.fn(),

View File

@ -17,6 +17,7 @@ import kebabCase from "lodash/kebabCase";
import orderBy from "lodash/orderBy"; import orderBy from "lodash/orderBy";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { DeploymentsRouteParams } from "../../../common/routes"; import type { DeploymentsRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -59,7 +60,7 @@ export class Deployments extends React.Component<DeploymentsProps> {
[columnId.name]: deployment => deployment.getName(), [columnId.name]: deployment => deployment.getName(),
[columnId.namespace]: deployment => deployment.getNs(), [columnId.namespace]: deployment => deployment.getNs(),
[columnId.replicas]: deployment => deployment.getReplicas(), [columnId.replicas]: deployment => deployment.getReplicas(),
[columnId.age]: deployment => deployment.getTimeDiffFromNow(), [columnId.age]: deployment => -deployment.getCreationTimestamp(),
[columnId.condition]: deployment => deployment.getConditionsText(), [columnId.condition]: deployment => deployment.getConditionsText(),
}} }}
searchFilters={[ searchFilters={[
@ -82,7 +83,7 @@ export class Deployments extends React.Component<DeploymentsProps> {
deployment.getNs(), deployment.getNs(),
this.renderPods(deployment), this.renderPods(deployment),
deployment.getReplicas(), deployment.getReplicas(),
deployment.getAge(), <KubeObjectAge key="age" object={deployment} />,
this.renderConditions(deployment), this.renderConditions(deployment),
]} ]}
/> />

View File

@ -14,6 +14,7 @@ import { KubeObjectListLayout } from "../kube-object-list-layout";
import kebabCase from "lodash/kebabCase"; import kebabCase from "lodash/kebabCase";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { JobsRouteParams } from "../../../common/routes"; import type { JobsRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -39,7 +40,7 @@ export class Jobs extends React.Component<JobsProps> {
[columnId.name]: job => job.getName(), [columnId.name]: job => job.getName(),
[columnId.namespace]: job => job.getNs(), [columnId.namespace]: job => job.getNs(),
[columnId.conditions]: job => job.getCondition() != null ? job.getCondition().type : "", [columnId.conditions]: job => job.getCondition() != null ? job.getCondition().type : "",
[columnId.age]: job => job.getTimeDiffFromNow(), [columnId.age]: job => -job.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
job => job.getSearchFields(), job => job.getSearchFields(),
@ -61,7 +62,7 @@ export class Jobs extends React.Component<JobsProps> {
job.getNs(), job.getNs(),
`${job.getCompletions()} / ${job.getDesiredCompletions()}`, `${job.getCompletions()} / ${job.getDesiredCompletions()}`,
<KubeObjectStatusIcon key="icon" object={job}/>, <KubeObjectStatusIcon key="icon" object={job}/>,
job.getAge(), <KubeObjectAge key="age" object={job} />,
condition && { condition && {
title: condition.type, title: condition.type,
className: kebabCase(condition.type), className: kebabCase(condition.type),

View File

@ -23,6 +23,7 @@ import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { Badge } from "../badge"; import { Badge } from "../badge";
import type { PodsRouteParams } from "../../../common/routes"; import type { PodsRouteParams } from "../../../common/routes";
import { getDetailsUrl } from "../kube-detail-params"; import { getDetailsUrl } from "../kube-detail-params";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -89,7 +90,7 @@ export class Pods extends React.Component<PodsProps> {
[columnId.owners]: pod => pod.getOwnerRefs().map(ref => ref.kind), [columnId.owners]: pod => pod.getOwnerRefs().map(ref => ref.kind),
[columnId.qos]: pod => pod.getQosClass(), [columnId.qos]: pod => pod.getQosClass(),
[columnId.node]: pod => pod.getNodeName(), [columnId.node]: pod => pod.getNodeName(),
[columnId.age]: pod => pod.getTimeDiffFromNow(), [columnId.age]: pod => -pod.getCreationTimestamp(),
[columnId.status]: pod => pod.getStatusMessage(), [columnId.status]: pod => pod.getStatusMessage(),
}} }}
searchFilters={[ searchFilters={[
@ -137,7 +138,7 @@ export class Pods extends React.Component<PodsProps> {
</Badge> </Badge>
: "", : "",
pod.getQosClass(), pod.getQosClass(),
pod.getAge(), <KubeObjectAge key="age" object={pod} />,
{ title: pod.getStatusMessage(), className: kebabCase(pod.getStatusMessage()) }, { title: pod.getStatusMessage(), className: kebabCase(pod.getStatusMessage()) },
]} ]}
/> />

View File

@ -82,6 +82,7 @@ const dummyReplicaSet: ReplicaSet = {
getName: jest.fn(), getName: jest.fn(),
getNs: jest.fn(), getNs: jest.fn(),
getAge: jest.fn(), getAge: jest.fn(),
getCreationTimestamp: jest.fn(),
getTimeDiffFromNow: jest.fn(), getTimeDiffFromNow: jest.fn(),
getFinalizers: jest.fn(), getFinalizers: jest.fn(),
getLabels: jest.fn(), getLabels: jest.fn(),

View File

@ -13,6 +13,7 @@ import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import type { ReplicaSetsRouteParams } from "../../../common/routes"; import type { ReplicaSetsRouteParams } from "../../../common/routes";
import { eventStore } from "../+events/event.store"; import { eventStore } from "../+events/event.store";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -41,7 +42,7 @@ export class ReplicaSets extends React.Component<ReplicaSetsProps> {
[columnId.desired]: replicaSet => replicaSet.getDesired(), [columnId.desired]: replicaSet => replicaSet.getDesired(),
[columnId.current]: replicaSet => replicaSet.getCurrent(), [columnId.current]: replicaSet => replicaSet.getCurrent(),
[columnId.ready]: replicaSet => replicaSet.getReady(), [columnId.ready]: replicaSet => replicaSet.getReady(),
[columnId.age]: replicaSet => replicaSet.getTimeDiffFromNow(), [columnId.age]: replicaSet => -replicaSet.getCreationTimestamp(),
}} }}
searchFilters={[ searchFilters={[
replicaSet => replicaSet.getSearchFields(), replicaSet => replicaSet.getSearchFields(),
@ -63,7 +64,7 @@ export class ReplicaSets extends React.Component<ReplicaSetsProps> {
replicaSet.getDesired(), replicaSet.getDesired(),
replicaSet.getCurrent(), replicaSet.getCurrent(),
replicaSet.getReady(), replicaSet.getReady(),
replicaSet.getAge(), <KubeObjectAge key="age" object={replicaSet} />,
]} ]}
/> />
); );

View File

@ -92,6 +92,7 @@ const dummyStatefulSet: StatefulSet = {
getName: jest.fn(), getName: jest.fn(),
getNs: jest.fn(), getNs: jest.fn(),
getAge: jest.fn(), getAge: jest.fn(),
getCreationTimestamp: jest.fn(),
getTimeDiffFromNow: jest.fn(), getTimeDiffFromNow: jest.fn(),
getFinalizers: jest.fn(), getFinalizers: jest.fn(),
getLabels: jest.fn(), getLabels: jest.fn(),

View File

@ -15,6 +15,7 @@ import { eventStore } from "../+events/event.store";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { StatefulSetsRouteParams } from "../../../common/routes"; import type { StatefulSetsRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId { enum columnId {
name = "name", name = "name",
@ -45,7 +46,7 @@ export class StatefulSets extends React.Component<StatefulSetsProps> {
sortingCallbacks={{ sortingCallbacks={{
[columnId.name]: statefulSet => statefulSet.getName(), [columnId.name]: statefulSet => statefulSet.getName(),
[columnId.namespace]: statefulSet => statefulSet.getNs(), [columnId.namespace]: statefulSet => statefulSet.getNs(),
[columnId.age]: statefulSet => statefulSet.getTimeDiffFromNow(), [columnId.age]: statefulSet => -statefulSet.getCreationTimestamp(),
[columnId.replicas]: statefulSet => statefulSet.getReplicas(), [columnId.replicas]: statefulSet => statefulSet.getReplicas(),
}} }}
searchFilters={[ searchFilters={[
@ -66,7 +67,7 @@ export class StatefulSets extends React.Component<StatefulSetsProps> {
this.renderPods(statefulSet), this.renderPods(statefulSet),
statefulSet.getReplicas(), statefulSet.getReplicas(),
<KubeObjectStatusIcon key="icon" object={statefulSet}/>, <KubeObjectStatusIcon key="icon" object={statefulSet}/>,
statefulSet.getAge(), <KubeObjectAge key="age" object={statefulSet} />,
]} ]}
/> />
); );

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { observer } from "mobx-react";
import { now } from "mobx-utils";
import React from "react";
import { formatDuration } from "../../utils";
export interface ReactiveDurationProps {
timestamp: string;
/**
* Whether the display string should prefer length over precision
* @default true
*/
compact?: boolean;
}
/**
* This function computes a resonable update
*/
function computeUpdateInterval(creationTimestampEpoch: number): number {
const seconds = Math.floor((Date.now() - creationTimestampEpoch) / 1000);
const minutes = Math.floor(seconds / 60);
if (minutes < 10) {
// Update every second
return 1000;
}
return 60 * 1000;
}
export const ReactiveDuration = observer(({ timestamp, compact = true }: ReactiveDurationProps) => {
const creationTimestamp = new Date(timestamp).getTime();
return (
<>
{formatDuration(now(computeUpdateInterval(creationTimestamp)) - creationTimestamp, compact)}
</>
);
});

View File

@ -12,6 +12,7 @@ import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { LocaleDate } from "../locale-date"; 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 { KubeObjectAge } from "../kube-object/age";
export interface KubeObjectMetaProps { export interface KubeObjectMetaProps {
object: KubeObject; object: KubeObject;
@ -44,14 +45,16 @@ export class KubeObjectMeta extends React.Component<KubeObjectMetaProps> {
const { const {
getNs, getLabels, getResourceVersion, selfLink, getAnnotations, getNs, getLabels, getResourceVersion, selfLink, getAnnotations,
getFinalizers, getId, getAge, getName, metadata: { creationTimestamp }, getFinalizers, getId, getName, metadata: { creationTimestamp },
} = object; } = object;
const ownerRefs = object.getOwnerRefs(); const ownerRefs = object.getOwnerRefs();
return ( return (
<> <>
<DrawerItem name="Created" hidden={this.isHidden("creationTimestamp")}> <DrawerItem name="Created" hidden={this.isHidden("creationTimestamp")}>
{getAge(true, false)} ago ({<LocaleDate date={creationTimestamp} />}) <KubeObjectAge object={object} compact={false} />
{" ago "}
({<LocaleDate date={creationTimestamp} />})
</DrawerItem> </DrawerItem>
<DrawerItem name="Name" hidden={this.isHidden("name")}> <DrawerItem name="Name" hidden={this.isHidden("name")}>
{getName()} {getName()}

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { ReactiveDuration } from "../duration/reactive-duration";
export interface KubeObjectAgeProps {
object: KubeObject;
/**
* Whether the display string should prefer length over precision
* @default true
*/
compact?: boolean;
}
export const KubeObjectAge = ({ object, compact = true }: KubeObjectAgeProps) => (
<ReactiveDuration timestamp={object.metadata.creationTimestamp} compact={compact} />
);

View File

@ -9285,6 +9285,11 @@ mobx-react@^7.2.1:
dependencies: dependencies:
mobx-react-lite "^3.2.0" mobx-react-lite "^3.2.0"
mobx-utils@^6.0.4:
version "6.0.4"
resolved "https://registry.yarnpkg.com/mobx-utils/-/mobx-utils-6.0.4.tgz#5283a466ece8de0ac36ae3cfa1b1c032ec302b37"
integrity sha512-CcTgFcCWN78eyRXU7OiKfhIVDEWFFoKdpfj49GIVcWykIQ4deXnaRnnKHElbVYFFgz1TOs8a3bDAq7qsSe864A==
mobx@^6.3.0: mobx@^6.3.0:
version "6.3.0" version "6.3.0"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.0.tgz#a8fb693c3047bdfcb1eaff9aa48e36a7eb084f96" resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.0.tgz#a8fb693c3047bdfcb1eaff9aa48e36a7eb084f96"