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-observable-history": "^2.0.3",
"mobx-react": "^7.2.1",
"mobx-utils": "^6.0.4",
"mock-fs": "^5.1.2",
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import { limitRangeStore } from "./limit-ranges.store";
import React from "react";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { LimitRangeRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId {
name = "name",
@ -32,9 +33,9 @@ export class LimitRanges extends React.Component<LimitRangesProps> {
className="LimitRanges"
store={limitRangeStore}
sortingCallbacks={{
[columnId.name]: item => item.getName(),
[columnId.namespace]: item => item.getNs(),
[columnId.age]: item => item.getTimeDiffFromNow(),
[columnId.name]: limitRange => limitRange.getName(),
[columnId.namespace]: limitRange => limitRange.getNs(),
[columnId.age]: limitRange => -limitRange.getCreationTimestamp(),
}}
searchFilters={[
item => item.getName(),
@ -51,7 +52,7 @@ export class LimitRanges extends React.Component<LimitRangesProps> {
limitRange.getName(),
<KubeObjectStatusIcon key="icon" object={limitRange}/>,
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 { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { ConfigMapsRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId {
name = "name",
@ -30,16 +31,17 @@ export class ConfigMaps extends React.Component<ConfigMapsProps> {
<KubeObjectListLayout
isConfigurable
tableId="configuration_configmaps"
className="ConfigMaps" store={configMapsStore}
className="ConfigMaps"
store={configMapsStore}
sortingCallbacks={{
[columnId.name]: item => item.getName(),
[columnId.namespace]: item => item.getNs(),
[columnId.keys]: item => item.getKeys(),
[columnId.age]: item => item.getTimeDiffFromNow(),
[columnId.name]: configMap => configMap.getName(),
[columnId.namespace]: configMap => configMap.getNs(),
[columnId.keys]: configMap => configMap.getKeys(),
[columnId.age]: configMap => -configMap.getCreationTimestamp(),
}}
searchFilters={[
item => item.getSearchFields(),
item => item.getKeys(),
configMap => configMap.getSearchFields(),
configMap => configMap.getKeys(),
]}
renderHeaderTitle="Config Maps"
renderTableHeader={[
@ -54,7 +56,7 @@ export class ConfigMaps extends React.Component<ConfigMapsProps> {
<KubeObjectStatusIcon key="icon" object={configMap}/>,
configMap.getNs(),
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 type { KubeObjectDetailsProps } from "../kube-object-details";
import { KubeObjectListLayout } from "../kube-object-list-layout";
import { KubeObjectAge } from "../kube-object/age";
enum columnId {
name = "name",
@ -42,7 +43,7 @@ export class PodDisruptionBudgets extends React.Component<PodDisruptionBudgetsPr
[columnId.maxUnavailable]: pdb => pdb.getMaxUnavailable(),
[columnId.currentHealthy]: pdb => pdb.getCurrentHealthy(),
[columnId.desiredHealthy]: pdb => pdb.getDesiredHealthy(),
[columnId.age]: pdb => pdb.getAge(),
[columnId.age]: pdb => -pdb.getCreationTimestamp(),
}}
searchFilters={[
pdb => pdb.getSearchFields(),
@ -58,8 +59,7 @@ export class PodDisruptionBudgets extends React.Component<PodDisruptionBudgetsPr
{ title: "Desired Healthy", className: "desired-healthy", sortBy: columnId.desiredHealthy, id: columnId.desiredHealthy },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={pdb => {
return [
renderTableContents={pdb => [
pdb.getName(),
<KubeObjectStatusIcon key="icon" object={pdb} />,
pdb.getNs(),
@ -67,9 +67,8 @@ export class PodDisruptionBudgets extends React.Component<PodDisruptionBudgetsPr
pdb.getMaxUnavailable(),
pdb.getCurrentHealthy(),
pdb.getDesiredHealthy(),
pdb.getAge(),
];
}}
<KubeObjectAge key="age" object={pdb} />,
]}
/>
);
}

View File

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

View File

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

View File

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

View File

@ -6,17 +6,16 @@
import "./crd-resources.scss";
import React from "react";
import jsonPath from "jsonpath";
import { value } from "jsonpath";
import { observer } from "mobx-react";
import type { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object-list-layout";
import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { computed, makeObservable } from "mobx";
import { crdStore } from "./crd.store";
import type { TableSortCallbacks } from "../table";
import { apiManager } from "../../../common/k8s-api/api-manager";
import { parseJsonPath } from "../../utils/jsonPath";
import type { CRDRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
export interface CustomResourceDefinitionResourcesProps extends RouteComponentProps<CRDRouteParams> {
}
@ -47,29 +46,13 @@ export class CustomResourceDefinitionResources extends React.Component<CustomRes
render() {
const { crd, store } = this;
if (!crd) return null;
if (!crd) {
return null;
}
const isNamespaced = crd.isNamespaced();
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 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 (
<KubeObjectListLayout
@ -78,9 +61,17 @@ export class CustomResourceDefinitionResources extends React.Component<CustomRes
tableId="crd_resources"
className="CrdResources"
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={[
item => item.getSearchFields(),
customResource => customResource.getSearchFields(),
]}
renderHeaderTitle={crd.getResourceKind()}
customizeHeader={({ searchProps, ...headerPlaceholders }) => ({
@ -93,36 +84,39 @@ export class CustomResourceDefinitionResources extends React.Component<CustomRes
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
isNamespaced && { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
...extraColumns.map(column => {
const { name } = column;
return {
...extraColumns.map(({ name }) => ({
title: name,
className: name.toLowerCase(),
sortBy: name,
id: name,
};
}),
})),
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={crdInstance => [
crdInstance.getName(),
isNamespaced && crdInstance.getNs(),
...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") {
value = JSON.stringify(value);
if (Array.isArray(rawValue) || typeof rawValue === "object") {
rawValue = JSON.stringify(rawValue);
}
return {
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 { apiManager } from "../../../common/k8s-api/api-manager";
import logger from "../../../common/logger";
import { ReactiveDuration } from "../duration/reactive-duration";
export interface EventDetailsProps extends KubeObjectDetailsProps<KubeEvent> {
}
@ -54,10 +55,14 @@ export class EventDetails extends React.Component<EventDetailsProps> {
{event.getSource()}
</DrawerItem>
<DrawerItem name="First seen">
{event.getFirstSeenTime()} ago (<LocaleDate date={event.firstTimestamp} />)
<ReactiveDuration timestamp={event.firstTimestamp} />
{" ago "}
(<LocaleDate date={event.firstTimestamp} />)
</DrawerItem>
<DrawerItem name="Last seen">
{event.getLastSeenTime()} ago (<LocaleDate date={event.lastTimestamp} />)
<ReactiveDuration timestamp={event.lastTimestamp} />
{" ago "}
(<LocaleDate date={event.lastTimestamp} />)
</DrawerItem>
<DrawerItem name="Count">
{count}

View File

@ -29,7 +29,7 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
protected sortItems(items: KubeEvent[]) {
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");
}

View File

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

View File

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

View File

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

View File

@ -33,6 +33,8 @@ import releaseInjectable from "./release.injectable";
import releaseDetailsInjectable from "./release-details.injectable";
import releaseValuesInjectable from "./release-values.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 {
hideDetails(): void;
@ -150,13 +152,12 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
);
}
renderResources() {
const { resources } = this.details;
if (!resources) return null;
const groups = groupBy(resources, item => item.kind);
const tables = Object.entries(groups).map(([kind, items]) => {
renderResources(resources: KubeObject[]) {
return (
<div className="resources">
{
Object.entries(groupBy(resources, item => item.kind))
.map(([kind, items]) => (
<React.Fragment key={kind}>
<SubTitle title={kind} />
<Table scrollable={false}>
@ -176,19 +177,21 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
<TableCell className="name">
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
</TableCell>
{namespace && <TableCell className="namespace">{namespace}</TableCell>}
<TableCell className="age">{item.getAge()}</TableCell>
{namespace && (
<TableCell className="namespace">
{namespace}
</TableCell>
)}
<TableCell className="age">
<KubeObjectAge key="age" object={item} />
</TableCell>
</TableRow>
);
})}
</Table>
</React.Fragment>
);
});
return (
<div className="resources">
{tables}
))
}
</div>
);
}
@ -200,6 +203,8 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
return <Spinner center/>;
}
const { resources } = this.details;
return (
<div>
<DrawerItem name="Chart" className="chart">
@ -236,7 +241,7 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
<DrawerTitle title="Notes"/>
{this.renderNotes()}
<DrawerTitle title="Resources"/>
{this.renderResources()}
{resources && this.renderResources(resources)}
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import { Badge } from "../badge";
import { serviceStore } from "./services.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { ServicesRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId {
name = "name",
@ -44,7 +45,7 @@ export class Services extends React.Component<ServicesProps> {
[columnId.ports]: service => (service.spec.ports || []).map(({ port }) => port)[0],
[columnId.clusterIp]: service => service.getClusterIp(),
[columnId.type]: service => service.getType(),
[columnId.age]: service => service.getTimeDiffFromNow(),
[columnId.age]: service => -service.getCreationTimestamp(),
[columnId.status]: service => service.getStatus(),
}}
searchFilters={[
@ -81,7 +82,7 @@ export class Services extends React.Component<ServicesProps> {
service.getPorts().join(", "),
externalIps.join(", ") || "-",
service.getSelector().map(label => <Badge key={label} label={label} />),
service.getAge(),
<KubeObjectAge key="age" object={service} />,
{ 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 { makeObservable, observable } from "mobx";
import isEmpty from "lodash/isEmpty";
import { KubeObjectAge } from "../kube-object/age";
enum columnId {
name = "name",
@ -191,7 +192,7 @@ export class NodesRoute extends React.Component<NodesRouteProps> {
[columnId.conditions]: node => node.getNodeConditionText(),
[columnId.taints]: node => node.getTaints().length,
[columnId.roles]: node => node.getRoleLabels(),
[columnId.age]: node => node.getTimeDiffFromNow(),
[columnId.age]: node => -node.getCreationTimestamp(),
[columnId.version]: node => node.getKubeletVersion(),
}}
searchFilters={[
@ -231,7 +232,7 @@ export class NodesRoute extends React.Component<NodesRouteProps> {
</>,
node.getRoleLabels(),
node.status.nodeInfo.kubeletVersion,
node.getAge(),
<KubeObjectAge key="age" object={node} />,
this.renderConditions(node),
];
}}

View File

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

View File

@ -12,6 +12,7 @@ import { KubeObjectListLayout } from "../kube-object-list-layout";
import { storageClassStore } from "./storage-class.store";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { StorageClassesRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId {
name = "name",
@ -34,14 +35,14 @@ export class StorageClasses extends React.Component<StorageClassesProps> {
className="StorageClasses"
store={storageClassStore}
sortingCallbacks={{
[columnId.name]: item => item.getName(),
[columnId.age]: item => item.getTimeDiffFromNow(),
[columnId.provisioner]: item => item.provisioner,
[columnId.reclaimPolicy]: item => item.reclaimPolicy,
[columnId.name]: storageClass => storageClass.getName(),
[columnId.age]: storageClass => -storageClass.getCreationTimestamp(),
[columnId.provisioner]: storageClass => storageClass.provisioner,
[columnId.reclaimPolicy]: storageClass => storageClass.reclaimPolicy,
}}
searchFilters={[
item => item.getSearchFields(),
item => item.provisioner,
storageClass => storageClass.getSearchFields(),
storageClass => storageClass.provisioner,
]}
renderHeaderTitle="Storage Classes"
renderTableHeader={[
@ -58,7 +59,7 @@ export class StorageClasses extends React.Component<StorageClassesProps> {
storageClass.provisioner,
storageClass.getReclaimPolicy(),
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 type { VolumeClaimsRouteParams } from "../../../common/routes";
import { getDetailsUrl } from "../kube-detail-params";
import { KubeObjectAge } from "../kube-object/age";
enum columnId {
name = "name",
@ -48,11 +49,11 @@ export class PersistentVolumeClaims extends React.Component<PersistentVolumeClai
[columnId.status]: pvc => pvc.getStatus(),
[columnId.size]: pvc => unitsToBytes(pvc.getStorage()),
[columnId.storageClass]: pvc => pvc.spec.storageClassName,
[columnId.age]: pvc => pvc.getTimeDiffFromNow(),
[columnId.age]: pvc => -pvc.getCreationTimestamp(),
}}
searchFilters={[
item => item.getSearchFields(),
item => item.getPods(podsStore.items).map(pod => pod.getName()),
pvc => pvc.getSearchFields(),
pvc => pvc.getPods(podsStore.items).map(pod => pod.getName()),
]}
renderHeaderTitle="Persistent Volume Claims"
renderTableHeader={[
@ -85,7 +86,7 @@ export class PersistentVolumeClaims extends React.Component<PersistentVolumeClai
{pod.getName()}
</Link>
)),
pvc.getAge(),
<KubeObjectAge key="age" object={pvc} />,
{ 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 { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { VolumesRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId {
name = "name",
@ -38,15 +39,15 @@ export class PersistentVolumes extends React.Component<PersistentVolumesProps> {
className="PersistentVolumes"
store={volumesStore}
sortingCallbacks={{
[columnId.name]: item => item.getName(),
[columnId.storageClass]: item => item.getStorageClass(),
[columnId.capacity]: item => item.getCapacity(true),
[columnId.status]: item => item.getStatus(),
[columnId.age]: item => item.getTimeDiffFromNow(),
[columnId.name]: volume => volume.getName(),
[columnId.storageClass]: volume => volume.getStorageClass(),
[columnId.capacity]: volume => volume.getCapacity(true),
[columnId.status]: volume => volume.getStatus(),
[columnId.age]: volume => -volume.getCreationTimestamp(),
}}
searchFilters={[
item => item.getSearchFields(),
item => item.getClaimRefName(),
volume => volume.getSearchFields(),
volume => volume.getClaimRefName(),
]}
renderHeaderTitle="Persistent Volumes"
renderTableHeader={[
@ -76,7 +77,7 @@ export class PersistentVolumes extends React.Component<PersistentVolumesProps> {
{claimRef.name}
</Link>
),
volume.getAge(),
<KubeObjectAge key="age" object={volume} />,
{ title: volume.getStatus(), className: volume.getStatus().toLowerCase() },
];
}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ import { KubeObjectListLayout } from "../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { CronJobsRouteParams } from "../../../common/routes";
import moment from "moment";
import { KubeObjectAge } from "../kube-object/age";
enum columnId {
name = "name",
@ -36,7 +37,8 @@ export class CronJobs extends React.Component<CronJobsProps> {
<KubeObjectListLayout
isConfigurable
tableId="workload_cronjobs"
className="CronJobs" store={cronJobStore}
className="CronJobs"
store={cronJobStore}
dependentStores={[jobStore, eventStore]}
sortingCallbacks={{
[columnId.name]: cronJob => cronJob.getName(),
@ -48,7 +50,7 @@ export class CronJobs extends React.Component<CronJobsProps> {
? moment().diff(cronJob.status.lastScheduleTime)
: 0
),
[columnId.age]: cronJob => cronJob.getTimeDiffFromNow(),
[columnId.age]: cronJob => -cronJob.getCreationTimestamp(),
}}
searchFilters={[
cronJob => cronJob.getSearchFields(),
@ -73,7 +75,7 @@ export class CronJobs extends React.Component<CronJobsProps> {
cronJob.getSuspendFlag(),
cronJobStore.getActiveJobsNum(cronJob),
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 { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { DaemonSetsRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId {
name = "name",
@ -39,13 +40,14 @@ export class DaemonSets extends React.Component<DaemonSetsProps> {
<KubeObjectListLayout
isConfigurable
tableId="workload_daemonsets"
className="DaemonSets" store={daemonSetStore}
className="DaemonSets"
store={daemonSetStore}
dependentStores={[podsStore, eventStore]} // status icon component uses event store
sortingCallbacks={{
[columnId.name]: daemonSet => daemonSet.getName(),
[columnId.namespace]: daemonSet => daemonSet.getNs(),
[columnId.pods]: daemonSet => this.getPodsLength(daemonSet),
[columnId.age]: daemonSet => daemonSet.getTimeDiffFromNow(),
[columnId.age]: daemonSet => -daemonSet.getCreationTimestamp(),
}}
searchFilters={[
daemonSet => daemonSet.getSearchFields(),
@ -68,7 +70,7 @@ export class DaemonSets extends React.Component<DaemonSetsProps> {
daemonSet.getNodeSelectors().map(selector => (
<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 { replicaSetStore } from "../+workloads-replicasets/replicasets.store";
import { showDetails } from "../kube-detail-params";
import { KubeObjectAge } from "../kube-object/age";
enum sortBy {
@ -71,8 +72,7 @@ export class DeploymentReplicaSets extends React.Component<DeploymentReplicaSets
<TableCell className="actions"/>
</TableHead>
{
replicaSets.map(replica => {
return (
replicaSets.map(replica => (
<TableRow
key={replica.getId()}
sortItem={replica}
@ -83,13 +83,12 @@ export class DeploymentReplicaSets extends React.Component<DeploymentReplicaSets
<TableCell className="warning"><KubeObjectStatusIcon key="icon" object={replica} /></TableCell>
<TableCell className="namespace">{replica.getNs()}</TableCell>
<TableCell className="pods">{this.getPodsLength(replica)}</TableCell>
<TableCell className="age">{replica.getAge()}</TableCell>
<TableCell className="age"><KubeObjectAge key="age" object={replica} /></TableCell>
<TableCell className="actions" onClick={stopPropagation}>
<ReplicaSetMenu object={replica} />
</TableCell>
</TableRow>
);
})
))
}
</Table>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@ -82,6 +82,7 @@ const dummyReplicaSet: ReplicaSet = {
getName: jest.fn(),
getNs: jest.fn(),
getAge: jest.fn(),
getCreationTimestamp: jest.fn(),
getTimeDiffFromNow: jest.fn(),
getFinalizers: 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 type { ReplicaSetsRouteParams } from "../../../common/routes";
import { eventStore } from "../+events/event.store";
import { KubeObjectAge } from "../kube-object/age";
enum columnId {
name = "name",
@ -41,7 +42,7 @@ export class ReplicaSets extends React.Component<ReplicaSetsProps> {
[columnId.desired]: replicaSet => replicaSet.getDesired(),
[columnId.current]: replicaSet => replicaSet.getCurrent(),
[columnId.ready]: replicaSet => replicaSet.getReady(),
[columnId.age]: replicaSet => replicaSet.getTimeDiffFromNow(),
[columnId.age]: replicaSet => -replicaSet.getCreationTimestamp(),
}}
searchFilters={[
replicaSet => replicaSet.getSearchFields(),
@ -63,7 +64,7 @@ export class ReplicaSets extends React.Component<ReplicaSetsProps> {
replicaSet.getDesired(),
replicaSet.getCurrent(),
replicaSet.getReady(),
replicaSet.getAge(),
<KubeObjectAge key="age" object={replicaSet} />,
]}
/>
);

View File

@ -92,6 +92,7 @@ const dummyStatefulSet: StatefulSet = {
getName: jest.fn(),
getNs: jest.fn(),
getAge: jest.fn(),
getCreationTimestamp: jest.fn(),
getTimeDiffFromNow: jest.fn(),
getFinalizers: 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 { KubeObjectStatusIcon } from "../kube-object-status-icon";
import type { StatefulSetsRouteParams } from "../../../common/routes";
import { KubeObjectAge } from "../kube-object/age";
enum columnId {
name = "name",
@ -45,7 +46,7 @@ export class StatefulSets extends React.Component<StatefulSetsProps> {
sortingCallbacks={{
[columnId.name]: statefulSet => statefulSet.getName(),
[columnId.namespace]: statefulSet => statefulSet.getNs(),
[columnId.age]: statefulSet => statefulSet.getTimeDiffFromNow(),
[columnId.age]: statefulSet => -statefulSet.getCreationTimestamp(),
[columnId.replicas]: statefulSet => statefulSet.getReplicas(),
}}
searchFilters={[
@ -66,7 +67,7 @@ export class StatefulSets extends React.Component<StatefulSetsProps> {
this.renderPods(statefulSet),
statefulSet.getReplicas(),
<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 { getDetailsUrl } from "../kube-detail-params";
import logger from "../../../common/logger";
import { KubeObjectAge } from "../kube-object/age";
export interface KubeObjectMetaProps {
object: KubeObject;
@ -44,14 +45,16 @@ export class KubeObjectMeta extends React.Component<KubeObjectMetaProps> {
const {
getNs, getLabels, getResourceVersion, selfLink, getAnnotations,
getFinalizers, getId, getAge, getName, metadata: { creationTimestamp },
getFinalizers, getId, getName, metadata: { creationTimestamp },
} = object;
const ownerRefs = object.getOwnerRefs();
return (
<>
<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 name="Name" hidden={this.isHidden("name")}>
{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:
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:
version "6.3.0"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.3.0.tgz#a8fb693c3047bdfcb1eaff9aa48e36a7eb084f96"