diff --git a/package.json b/package.json index f04072e55c..e4a60b5d71 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/k8s-api/endpoints/events.api.ts b/src/common/k8s-api/endpoints/events.api.ts index f4218ba8c0..f2f73ce383 100644 --- a/src/common/k8s-api/endpoints/events.api.ts +++ b/src/common/k8s-api/endpoints/events.api.ts @@ -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 `` 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 `` instead + */ getLastSeenTime() { const diff = moment().diff(this.lastTimestamp); diff --git a/src/common/k8s-api/kube-object.ts b/src/common/k8s-api/kube-object.ts index 7098c98847..afabeaf638 100644 --- a/src/common/k8s-api/kube-object.ts +++ b/src/common/k8s-api/kube-object.ts @@ -261,10 +261,28 @@ export class KubeObject React.ReactElement; + ageMs: number; } enum sortBy { @@ -40,63 +41,42 @@ enum sortBy { @observer export class ClusterIssues extends React.Component { - 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, - 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; + @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, + renderAge: () => , + ageMs: -node.getCreationTimestamp(), + })) + )), + ...eventStore.getWarnings().map(warning => ({ + getId: () => warning.involvedObject.uid, + getName: () => warning.involvedObject.name, + renderAge: () => , + 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 ( { {kind} - {age} + {renderAge()} ); @@ -143,15 +123,18 @@ export class ClusterIssues extends React.Component { return ( <> - {" "} - <>Warnings: {warnings.length} + Warnings: {warnings.length} warning.kind, + [sortBy.object]: warning => warning.getName(), + [sortBy.age]: warning => warning.ageMs, + }} sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }} sortSyncWithUrl={false} getTableRow={this.getTableRow} diff --git a/src/renderer/components/+config-autoscalers/hpa.tsx b/src/renderer/components/+config-autoscalers/hpa.tsx index f0b0e98265..0f99dd1d78 100644 --- a/src/renderer/components/+config-autoscalers/hpa.tsx +++ b/src/renderer/components/+config-autoscalers/hpa.tsx @@ -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 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 { - if (!isReady) return null; - - return ( + , + hpa.getConditions() + .filter(({ isReady }) => isReady) + .map(({ type, tooltip }) => ( - ); - }), + )), ]} /> ); diff --git a/src/renderer/components/+config-limit-ranges/limit-ranges.tsx b/src/renderer/components/+config-limit-ranges/limit-ranges.tsx index db5c98e0d5..285e9533c7 100644 --- a/src/renderer/components/+config-limit-ranges/limit-ranges.tsx +++ b/src/renderer/components/+config-limit-ranges/limit-ranges.tsx @@ -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 { 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 { limitRange.getName(), , limitRange.getNs(), - limitRange.getAge(), + , ]} /> ); diff --git a/src/renderer/components/+config-maps/config-maps.tsx b/src/renderer/components/+config-maps/config-maps.tsx index 7d206f33d7..3dc189560e 100644 --- a/src/renderer/components/+config-maps/config-maps.tsx +++ b/src/renderer/components/+config-maps/config-maps.tsx @@ -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 { 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 { , configMap.getNs(), configMap.getKeys().join(", "), - configMap.getAge(), + , ]} /> ); diff --git a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx index 5556f957a1..74f685df59 100644 --- a/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx +++ b/src/renderer/components/+config-pod-disruption-budgets/pod-disruption-budgets.tsx @@ -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 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,18 +59,16 @@ export class PodDisruptionBudgets extends React.Component { - return [ - pdb.getName(), - , - pdb.getNs(), - pdb.getMinAvailable(), - pdb.getMaxUnavailable(), - pdb.getCurrentHealthy(), - pdb.getDesiredHealthy(), - pdb.getAge(), - ]; - }} + renderTableContents={pdb => [ + pdb.getName(), + , + pdb.getNs(), + pdb.getMinAvailable(), + pdb.getMaxUnavailable(), + pdb.getCurrentHealthy(), + pdb.getDesiredHealthy(), + , + ]} /> ); } diff --git a/src/renderer/components/+config-resource-quotas/resource-quotas.tsx b/src/renderer/components/+config-resource-quotas/resource-quotas.tsx index 823eb8ced9..29a79d6bcc 100644 --- a/src/renderer/components/+config-resource-quotas/resource-quotas.tsx +++ b/src/renderer/components/+config-resource-quotas/resource-quotas.tsx @@ -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 { 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 { resourceQuota.getName(), , resourceQuota.getNs(), - resourceQuota.getAge(), + , ]} addRemoveButtons={{ onAdd: () => AddQuotaDialog.open(), diff --git a/src/renderer/components/+config-secrets/secrets.tsx b/src/renderer/components/+config-secrets/secrets.tsx index 07b737450c..9df906187e 100644 --- a/src/renderer/components/+config-secrets/secrets.tsx +++ b/src/renderer/components/+config-secrets/secrets.tsx @@ -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 { 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 { secret.getLabels().map(label => ), secret.getKeys().join(", "), secret.type, - secret.getAge(), + , ]} addRemoveButtons={{ onAdd: () => AddSecretDialog.open(), diff --git a/src/renderer/components/+custom-resources/crd-list.tsx b/src/renderer/components/+custom-resources/crd-list.tsx index a14b472d2f..8a3277ce21 100644 --- a/src/renderer/components/+custom-resources/crd-list.tsx +++ b/src/renderer/components/+custom-resources/crd-list.tsx @@ -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({ name: "groups", @@ -63,12 +62,6 @@ export class CustomResourceDefinitions extends React.Component { render() { const { items, selectedGroups } = this; - const sortingCallbacks: TableSortCallbacks = { - [columnId.kind]: crd => crd.getResourceKind(), - [columnId.group]: crd => crd.getGroup(), - [columnId.version]: crd => crd.getVersion(), - [columnId.scope]: crd => crd.getScope(), - }; return ( 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(), + , ]} /> ); diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index 8c029e92c1..0669f5ff86 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -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 { } @@ -47,29 +46,13 @@ export class CustomResourceDefinitionResources extends React.Component = { - [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 =

Failed to load {crd.getPluralName()}

; - const failedToLoadMessage = version.served - ? loadFailedPrefix - : ( - <> - {loadFailedPrefix} -

Prefered version ({crd.getGroup()}/{version.name}) is not served

- - ); return ( 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 { - const { name } = column; - - return { - title: name, - className: name.toLowerCase(), - sortBy: name, - id: name, - }; - }), + ...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(), + , ]} - failedToLoadMessage={failedToLoadMessage} + failedToLoadMessage={( + <> +

Failed to load {crd.getPluralName()}

+ {!version.served && ( +

Prefered version ({crd.getGroup()}/{version.name}) is not served

+ )} + + )} /> ); } diff --git a/src/renderer/components/+events/event-details.tsx b/src/renderer/components/+events/event-details.tsx index 74e711f839..28590d433e 100644 --- a/src/renderer/components/+events/event-details.tsx +++ b/src/renderer/components/+events/event-details.tsx @@ -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 { } @@ -54,10 +55,14 @@ export class EventDetails extends React.Component { {event.getSource()} - {event.getFirstSeenTime()} ago () + + {" ago "} + () - {event.getLastSeenTime()} ago () + + {" ago "} + () {count} diff --git a/src/renderer/components/+events/event.store.ts b/src/renderer/components/+events/event.store.ts index 942d3eb6e9..61d5ab7a08 100644 --- a/src/renderer/components/+events/event.store.ts +++ b/src/renderer/components/+events/event.store.ts @@ -29,7 +29,7 @@ export class EventStore extends KubeObjectStore { 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"); } diff --git a/src/renderer/components/+events/events.tsx b/src/renderer/components/+events/events.tsx index 332007e4dd..bdf83b750d 100644 --- a/src/renderer/components/+events/events.tsx +++ b/src/renderer/components/+events/events.tsx @@ -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 = { @observer export class Events extends React.Component { static defaultProps = defaultProps as object; - now = Date.now(); @observable sorting: TableSortParams = { sortBy: columnId.age, @@ -59,8 +60,8 @@ export class Events extends React.Component { [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 { , event.getSource(), event.count, - event.getAge(), - event.getLastSeenTime(), + , + , ]; }} /> diff --git a/src/renderer/components/+events/kube-event-details.tsx b/src/renderer/components/+events/kube-event-details.tsx index f621c02eae..8dd3fb17ee 100644 --- a/src/renderer/components/+events/kube-event-details.tsx +++ b/src/renderer/components/+events/kube-event-details.tsx @@ -51,44 +51,34 @@ class NonInjectedKubeEventDetails extends React.Component - Events - - ); - } - return (
Events -
- {events.map(evt => { - const { message, count, lastTimestamp, involvedObject } = evt; - - return ( -
-
- {message} + {events.length > 0 && ( +
+ {events.map(event => ( +
+
+ {event.message}
- {evt.getSource()} + {event.getSource()} - {count} + {event.count} - {involvedObject.fieldPath} + {event.involvedObject.fieldPath} - +
- ); - })} -
+ ))} +
+ )}
); } diff --git a/src/renderer/components/+events/kube-event-icon.tsx b/src/renderer/components/+events/kube-event-icon.tsx index 7b6f224847..80cb59d9d6 100644 --- a/src/renderer/components/+events/kube-event-icon.tsx +++ b/src/renderer/components/+events/kube-event-icon.tsx @@ -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 {
{event.message}
- {event.getAge(undefined, undefined, true)} +
), diff --git a/src/renderer/components/+helm-releases/release-details/release-details.tsx b/src/renderer/components/+helm-releases/release-details/release-details.tsx index 6a2a71a8fd..4346b11691 100644 --- a/src/renderer/components/+helm-releases/release-details/release-details.tsx +++ b/src/renderer/components/+helm-releases/release-details/release-details.tsx @@ -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,45 +152,46 @@ class NonInjectedReleaseDetails extends Component item.kind); - const tables = Object.entries(groups).map(([kind, items]) => { - return ( - - -
- - Name - {items[0].getNs() && Namespace} - Age - - {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 ( - - - {detailsUrl ? {name} : name} - - {namespace && {namespace}} - {item.getAge()} - - ); - })} -
- - ); - }); - + renderResources(resources: KubeObject[]) { return (
- {tables} + { + Object.entries(groupBy(resources, item => item.kind)) + .map(([kind, items]) => ( + + + + + Name + {items[0].getNs() && Namespace} + Age + + {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 ( + + + {detailsUrl ? {name} : name} + + {namespace && ( + + {namespace} + + )} + + + + + ); + })} +
+
+ )) + }
); } @@ -200,6 +203,8 @@ class NonInjectedReleaseDetails extends Component; } + const { resources } = this.details; + return (
@@ -236,7 +241,7 @@ class NonInjectedReleaseDetails extends Component {this.renderNotes()} - {this.renderResources()} + {resources && this.renderResources(resources)}
); } diff --git a/src/renderer/components/+namespaces/route.tsx b/src/renderer/components/+namespaces/route.tsx index a40b6cb981..b1bf0601d0 100644 --- a/src/renderer/components/+namespaces/route.tsx +++ b/src/renderer/components/+namespaces/route.tsx @@ -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(), - , - item.getLabels().map(label => ), - item.getAge(), - { title: item.getStatus(), className: item.getStatus().toLowerCase() }, + renderTableContents={namespace => [ + namespace.getName(), + , + namespace.getLabels().map(label => ), + , + { title: namespace.getStatus(), className: namespace.getStatus().toLowerCase() }, ]} addRemoveButtons={{ addTooltip: "Add Namespace", diff --git a/src/renderer/components/+network-endpoints/endpoints.tsx b/src/renderer/components/+network-endpoints/endpoints.tsx index f5d5b31911..0c5595be54 100644 --- a/src/renderer/components/+network-endpoints/endpoints.tsx +++ b/src/renderer/components/+network-endpoints/endpoints.tsx @@ -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 { 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 { , endpoint.getNs(), endpoint.toString(), - endpoint.getAge(), + , ]} tableProps={{ customRowHeights: (item, lineHeight, paddings) => { diff --git a/src/renderer/components/+network-ingresses/ingresses.tsx b/src/renderer/components/+network-ingresses/ingresses.tsx index 21aa2ca207..d86fb98079 100644 --- a/src/renderer/components/+network-ingresses/ingresses.tsx +++ b/src/renderer/components/+network-ingresses/ingresses.tsx @@ -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 { 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 { ingress.getNs(), ingress.getLoadBalancers().map(lb =>

{lb}

), ingress.getRoutes().map(route =>

{route}

), - ingress.getAge(), + , ]} tableProps={{ customRowHeights: (item, lineHeight, paddings) => { diff --git a/src/renderer/components/+network-policies/network-policies.tsx b/src/renderer/components/+network-policies/network-policies.tsx index db3701ae45..c226f090d6 100644 --- a/src/renderer/components/+network-policies/network-policies.tsx +++ b/src/renderer/components/+network-policies/network-policies.tsx @@ -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 { 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 { { title: "Policy Types", className: "type", id: columnId.types }, { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, ]} - renderTableContents={item => [ - item.getName(), - , - item.getNs(), - item.getTypes().join(", "), - item.getAge(), + renderTableContents={networkPolicy => [ + networkPolicy.getName(), + , + networkPolicy.getNs(), + networkPolicy.getTypes().join(", "), + , ]} /> ); diff --git a/src/renderer/components/+network-services/services.tsx b/src/renderer/components/+network-services/services.tsx index 78cb97907c..57107f1fc2 100644 --- a/src/renderer/components/+network-services/services.tsx +++ b/src/renderer/components/+network-services/services.tsx @@ -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 { [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 { service.getPorts().join(", "), externalIps.join(", ") || "-", service.getSelector().map(label => ), - service.getAge(), + , { title: service.getStatus(), className: service.getStatus().toLowerCase() }, ]; }} diff --git a/src/renderer/components/+nodes/route.tsx b/src/renderer/components/+nodes/route.tsx index a4b4cbb660..9ca6874743 100644 --- a/src/renderer/components/+nodes/route.tsx +++ b/src/renderer/components/+nodes/route.tsx @@ -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 { [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 { , node.getRoleLabels(), node.status.nodeInfo.kubeletVersion, - node.getAge(), + , this.renderConditions(node), ]; }} diff --git a/src/renderer/components/+pod-security-policies/pod-security-policies.tsx b/src/renderer/components/+pod-security-policies/pod-security-policies.tsx index 1e596aa258..341db9cc35 100644 --- a/src/renderer/components/+pod-security-policies/pod-security-policies.tsx +++ b/src/renderer/components/+pod-security-policies/pod-security-policies.tsx @@ -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(), - , - item.isPrivileged() ? "Yes" : "No", - item.getVolumes().join(", "), - item.getAge(), - ]; - }} + renderTableContents={podSecurityPolicy => [ + podSecurityPolicy.getName(), + , + podSecurityPolicy.isPrivileged() ? "Yes" : "No", + podSecurityPolicy.getVolumes().join(", "), + , + ]} /> ); } diff --git a/src/renderer/components/+storage-classes/storage-classes.tsx b/src/renderer/components/+storage-classes/storage-classes.tsx index 06c646c7c6..5da1239b43 100644 --- a/src/renderer/components/+storage-classes/storage-classes.tsx +++ b/src/renderer/components/+storage-classes/storage-classes.tsx @@ -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 { 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 { storageClass.provisioner, storageClass.getReclaimPolicy(), storageClass.isDefault() ? "Yes" : null, - storageClass.getAge(), + , ]} /> ); diff --git a/src/renderer/components/+storage-volume-claims/volume-claims.tsx b/src/renderer/components/+storage-volume-claims/volume-claims.tsx index 30f0077901..4e3da7fa24 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claims.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claims.tsx @@ -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 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 )), - pvc.getAge(), + , { title: pvc.getStatus(), className: pvc.getStatus().toLowerCase() }, ]; }} diff --git a/src/renderer/components/+storage-volumes/volumes.tsx b/src/renderer/components/+storage-volumes/volumes.tsx index 0c458ea567..ab44b4cf3f 100644 --- a/src/renderer/components/+storage-volumes/volumes.tsx +++ b/src/renderer/components/+storage-volumes/volumes.tsx @@ -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 { 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 { {claimRef.name} ), - volume.getAge(), + , { title: volume.getStatus(), className: volume.getStatus().toLowerCase() }, ]; }} diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/view.tsx b/src/renderer/components/+user-management/+cluster-role-bindings/view.tsx index e24a73243f..48fc0840a0 100644 --- a/src/renderer/components/+user-management/+cluster-role-bindings/view.tsx +++ b/src/renderer/components/+user-management/+cluster-role-bindings/view.tsx @@ -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 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, binding.getSubjectNames(), - binding.getAge(), + , ]} addRemoveButtons={{ onAdd: () => ClusterRoleBindingDialog.open(), diff --git a/src/renderer/components/+user-management/+cluster-roles/view.tsx b/src/renderer/components/+user-management/+cluster-roles/view.tsx index ca2a8755d0..0b979145d6 100644 --- a/src/renderer/components/+user-management/+cluster-roles/view.tsx +++ b/src/renderer/components/+user-management/+cluster-roles/view.tsx @@ -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 { 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 { renderTableContents={clusterRole => [ clusterRole.getName(), , - clusterRole.getAge(), + , ]} addRemoveButtons={{ onAdd: () => AddClusterRoleDialog.open(), diff --git a/src/renderer/components/+user-management/+role-bindings/view.tsx b/src/renderer/components/+user-management/+role-bindings/view.tsx index 60a0307df4..5ef4099afc 100644 --- a/src/renderer/components/+user-management/+role-bindings/view.tsx +++ b/src/renderer/components/+user-management/+role-bindings/view.tsx @@ -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 { [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 { , binding.getNs(), binding.getSubjectNames(), - binding.getAge(), + , ]} addRemoveButtons={{ onAdd: () => RoleBindingDialog.open(), diff --git a/src/renderer/components/+user-management/+roles/view.tsx b/src/renderer/components/+user-management/+roles/view.tsx index 4939642e95..63e77c3e04 100644 --- a/src/renderer/components/+user-management/+roles/view.tsx +++ b/src/renderer/components/+user-management/+roles/view.tsx @@ -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 { 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 { role.getName(), , role.getNs(), - role.getAge(), + , ]} addRemoveButtons={{ onAdd: () => AddRoleDialog.open(), diff --git a/src/renderer/components/+user-management/+service-accounts/view.tsx b/src/renderer/components/+user-management/+service-accounts/view.tsx index bc768f5874..5c43716ed5 100644 --- a/src/renderer/components/+user-management/+service-accounts/view.tsx +++ b/src/renderer/components/+user-management/+service-accounts/view.tsx @@ -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 { 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 { account.getName(), , account.getNs(), - account.getAge(), + , ]} addRemoveButtons={{ onAdd: () => CreateServiceAccountDialog.open(), diff --git a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx index 5b0dff6ce5..7a818543af 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjobs.tsx +++ b/src/renderer/components/+workloads-cronjobs/cronjobs.tsx @@ -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 { cronJob.getName(), @@ -48,7 +50,7 @@ export class CronJobs extends React.Component { ? 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 { cronJob.getSuspendFlag(), cronJobStore.getActiveJobsNum(cronJob), cronJob.getLastScheduleTime(), - cronJob.getAge(), + , ]} /> ); diff --git a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx index 6aad52d788..37293daa05 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonsets.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonsets.tsx @@ -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 { 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 { daemonSet.getNodeSelectors().map(selector => ( )), - daemonSet.getAge(), + , ]} /> ); diff --git a/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx b/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx index 2bb1510f19..110b1da86e 100644 --- a/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-replicasets.tsx @@ -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,25 +72,23 @@ export class DeploymentReplicaSets extends React.Component { - replicaSets.map(replica => { - return ( - showDetails(replica.selfLink, false))} - > - {replica.getName()} - - {replica.getNs()} - {this.getPodsLength(replica)} - {replica.getAge()} - - - - - ); - }) + replicaSets.map(replica => ( + showDetails(replica.selfLink, false))} + > + {replica.getName()} + + {replica.getNs()} + {this.getPodsLength(replica)} + + + + + + )) } diff --git a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx index f9c7e143a5..61cd202094 100644 --- a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx @@ -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(), diff --git a/src/renderer/components/+workloads-deployments/deployments.tsx b/src/renderer/components/+workloads-deployments/deployments.tsx index 3dcbeac0b8..44f1d55b8e 100644 --- a/src/renderer/components/+workloads-deployments/deployments.tsx +++ b/src/renderer/components/+workloads-deployments/deployments.tsx @@ -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 { [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 { deployment.getNs(), this.renderPods(deployment), deployment.getReplicas(), - deployment.getAge(), + , this.renderConditions(deployment), ]} /> diff --git a/src/renderer/components/+workloads-jobs/jobs.tsx b/src/renderer/components/+workloads-jobs/jobs.tsx index 15a49bafb0..d6d1e47c5b 100644 --- a/src/renderer/components/+workloads-jobs/jobs.tsx +++ b/src/renderer/components/+workloads-jobs/jobs.tsx @@ -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 { [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 { job.getNs(), `${job.getCompletions()} / ${job.getDesiredCompletions()}`, , - job.getAge(), + , condition && { title: condition.type, className: kebabCase(condition.type), diff --git a/src/renderer/components/+workloads-pods/pods.tsx b/src/renderer/components/+workloads-pods/pods.tsx index 2780a5f9d7..b3f57db369 100644 --- a/src/renderer/components/+workloads-pods/pods.tsx +++ b/src/renderer/components/+workloads-pods/pods.tsx @@ -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 { [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 { : "", pod.getQosClass(), - pod.getAge(), + , { title: pod.getStatusMessage(), className: kebabCase(pod.getStatusMessage()) }, ]} /> diff --git a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx index ed987c4f8b..540237a377 100755 --- a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx +++ b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx @@ -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(), diff --git a/src/renderer/components/+workloads-replicasets/replicasets.tsx b/src/renderer/components/+workloads-replicasets/replicasets.tsx index 1a8b68898f..08d597efb6 100644 --- a/src/renderer/components/+workloads-replicasets/replicasets.tsx +++ b/src/renderer/components/+workloads-replicasets/replicasets.tsx @@ -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 { [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 { replicaSet.getDesired(), replicaSet.getCurrent(), replicaSet.getReady(), - replicaSet.getAge(), + , ]} /> ); diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx index c76aba55b7..f2d4998dd0 100755 --- a/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx @@ -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(), diff --git a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx index 135e563c4a..4aed8d3a6f 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulsets.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulsets.tsx @@ -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 { 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 { this.renderPods(statefulSet), statefulSet.getReplicas(), , - statefulSet.getAge(), + , ]} /> ); diff --git a/src/renderer/components/duration/reactive-duration.tsx b/src/renderer/components/duration/reactive-duration.tsx new file mode 100644 index 0000000000..9210f48de9 --- /dev/null +++ b/src/renderer/components/duration/reactive-duration.tsx @@ -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)} + + ); +}); diff --git a/src/renderer/components/kube-object-meta/kube-object-meta.tsx b/src/renderer/components/kube-object-meta/kube-object-meta.tsx index 36064ee933..b6c63db0f8 100644 --- a/src/renderer/components/kube-object-meta/kube-object-meta.tsx +++ b/src/renderer/components/kube-object-meta/kube-object-meta.tsx @@ -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 { const { getNs, getLabels, getResourceVersion, selfLink, getAnnotations, - getFinalizers, getId, getAge, getName, metadata: { creationTimestamp }, + getFinalizers, getId, getName, metadata: { creationTimestamp }, } = object; const ownerRefs = object.getOwnerRefs(); return ( <>