diff --git a/src/renderer/api/kube-object.ts b/src/renderer/api/kube-object.ts index d16d65b6da..25d3445525 100644 --- a/src/renderer/api/kube-object.ts +++ b/src/renderer/api/kube-object.ts @@ -122,12 +122,15 @@ export class KubeObject implements ItemObject { return this.metadata.namespace || undefined; } - // todo: refactor with named arguments + getTimeDiffFromNow(): number { + return new Date().getTime() - new Date(this.metadata.creationTimestamp).getTime(); + } + getAge(humanize = true, compact = true, fromNow = false) { if (fromNow) { - return moment(this.metadata.creationTimestamp).fromNow(); + return moment(this.metadata.creationTimestamp).fromNow(); // "string" } - const diff = new Date().getTime() - new Date(this.metadata.creationTimestamp).getTime(); + const diff = this.getTimeDiffFromNow(); if (humanize) { return formatDuration(diff, compact); diff --git a/src/renderer/components/+events/event.store.ts b/src/renderer/components/+events/event.store.ts index d6090be947..03d272643f 100644 --- a/src/renderer/components/+events/event.store.ts +++ b/src/renderer/components/+events/event.store.ts @@ -20,8 +20,8 @@ export class EventStore extends KubeObjectStore { protected sortItems(items: KubeEvent[]) { return super.sortItems(items, [ - event => event.metadata.creationTimestamp - ], "desc"); + event => event.getTimeDiffFromNow(), // keep events order as timeline ("fresh" on top) + ], "asc"); } getEventsByObject(obj: KubeObject): KubeEvent[] { diff --git a/src/renderer/components/+events/events.scss b/src/renderer/components/+events/events.scss index 8b84ffd9fc..80cedb8ff7 100644 --- a/src/renderer/components/+events/events.scss +++ b/src/renderer/components/+events/events.scss @@ -1,4 +1,10 @@ .Events { + h5.title { + .events-count { + font-size: $font-size; + } + } + .Table { .TableCell { &.message { diff --git a/src/renderer/components/+events/events.tsx b/src/renderer/components/+events/events.tsx index f8c48cfe09..f54f556aeb 100644 --- a/src/renderer/components/+events/events.tsx +++ b/src/renderer/components/+events/events.tsx @@ -1,11 +1,14 @@ import "./events.scss"; import React, { Fragment } from "react"; +import { computed, observable } from "mobx"; import { observer } from "mobx-react"; +import { orderBy } from "lodash"; import { TabLayout } from "../layout/tab-layout"; -import { eventStore } from "./event.store"; +import { EventStore, eventStore } from "./event.store"; import { getDetailsUrl, KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object"; import { KubeEvent } from "../../api/endpoints/events.api"; +import { TableSortCallbacks, TableSortParams } from "../table"; import { Tooltip } from "../tooltip"; import { Link } from "react-router-dom"; import { cssNames, IClassName, stopPropagation } from "../../utils"; @@ -37,23 +40,60 @@ const defaultProps: Partial = { export class Events extends React.Component { static defaultProps = defaultProps as object; - get store() { + private sortingCallbacks: TableSortCallbacks = { + [columnId.namespace]: (event: KubeEvent) => event.getNs(), + [columnId.type]: (event: KubeEvent) => event.type, + [columnId.object]: (event: KubeEvent) => event.involvedObject.name, + [columnId.count]: (event: KubeEvent) => event.count, + [columnId.age]: (event: KubeEvent) => event.getTimeDiffFromNow(), + }; + + @observable sorting: TableSortParams = { + sortBy: columnId.age, + orderBy: "asc", + }; + + get store(): EventStore { return eventStore; } - get items() { - return eventStore.contextItems; + @computed get items(): KubeEvent[] { + const items = this.store.contextItems; + const { sortBy, orderBy: order } = this.sorting; + + // we must sort items before passing to "KubeObjectListLayout -> Table" + // to make it work with "compact=true" (proper table sorting actions + initial items) + return orderBy(items, this.sortingCallbacks[sortBy], order as any); + } + + @computed get visibleItems(): KubeEvent[] { + const { compact, compactLimit } = this.props; + + if (compact) { + return this.items.slice(0, compactLimit); + } + + return this.items; + } + + @computed get header(): React.ReactNode { + const { items, visibleItems } = this; + const allEventsAreShown = visibleItems.length === items.length; + + if (this.props.compact && !allEventsAreShown) { + return <> + Events + ({visibleItems.length} of {items.length}) + + ; + } + + return <>Events; } render() { - const { store, items } = this; + const { store, visibleItems, header, sortingCallbacks, sorting } = this; const { compact, compactLimit, className, ...layoutProps } = this.props; - const visibleItems = compact ? items.slice(0, compactLimit) : items; - const allEventsAreShown = visibleItems.length === items.length; - - const compactModeHeader = <> - Events ({visibleItems.length} of {items.length}) - ; const events = ( { isSelectable={false} items={visibleItems} virtual={!compact} - renderHeaderTitle={compact && !allEventsAreShown ? compactModeHeader : "Events"} + renderHeaderTitle={header} + sortingCallbacks={sortingCallbacks} tableProps={{ sortSyncWithUrl: false, - sortByDefault: { - sortBy: columnId.type, - orderBy: "desc", // show "Warning" events at the top - }, - }} - sortingCallbacks={{ - [columnId.namespace]: (event: KubeEvent) => event.getNs(), - [columnId.type]: (event: KubeEvent) => event.type, - [columnId.object]: (event: KubeEvent) => event.involvedObject.name, - [columnId.count]: (event: KubeEvent) => event.count, - [columnId.age]: (event: KubeEvent) => event.metadata.creationTimestamp, + sortByDefault: sorting, + onSort: params => this.sorting = params, }} searchFilters={[ (event: KubeEvent) => event.getSearchFields(), 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 e3d18669f9..da3ac55e07 100644 --- a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx @@ -4,9 +4,9 @@ import "@testing-library/jest-dom/extend-expect"; import { DeploymentScaleDialog } from "./deployment-scale-dialog"; jest.mock("../../api/endpoints"); -import { deploymentApi } from "../../api/endpoints"; +import { Deployment, deploymentApi } from "../../api/endpoints"; -const dummyDeployment = { +const dummyDeployment: Deployment = { apiVersion: "v1", kind: "dummy", metadata: { @@ -83,6 +83,7 @@ const dummyDeployment = { getName: jest.fn(), getNs: jest.fn(), getAge: jest.fn(), + getTimeDiffFromNow: jest.fn(), getFinalizers: jest.fn(), getLabels: jest.fn(), getAnnotations: jest.fn(), 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 804b7c344f..131bc08ca2 100755 --- a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx +++ b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx @@ -4,9 +4,9 @@ jest.mock("../../api/endpoints"); import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog"; import { render, waitFor, fireEvent } from "@testing-library/react"; import React from "react"; -import { replicaSetApi } from "../../api/endpoints/replica-set.api"; +import { ReplicaSet, replicaSetApi } from "../../api/endpoints/replica-set.api"; -const dummyReplicaSet = { +const dummyReplicaSet: ReplicaSet = { apiVersion: "v1", kind: "dummy", metadata: { @@ -67,7 +67,6 @@ const dummyReplicaSet = { getCurrent: jest.fn(), getReady: jest.fn(), getImages: jest.fn(), - getReplicas: jest.fn(), getSelectors: jest.fn(), getTemplateLabels: jest.fn(), getAffinity: jest.fn(), @@ -79,6 +78,7 @@ const dummyReplicaSet = { getName: jest.fn(), getNs: jest.fn(), getAge: jest.fn(), + getTimeDiffFromNow: jest.fn(), getFinalizers: jest.fn(), getLabels: jest.fn(), getAnnotations: jest.fn(), 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 c0a26dbd52..faf94b995d 100755 --- a/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx @@ -1,12 +1,12 @@ import "@testing-library/jest-dom/extend-expect"; jest.mock("../../api/endpoints"); -import { statefulSetApi } from "../../api/endpoints"; +import { StatefulSet, statefulSetApi } from "../../api/endpoints"; import { StatefulSetScaleDialog } from "./statefulset-scale-dialog"; import { render, waitFor, fireEvent } from "@testing-library/react"; import React from "react"; -const dummyStatefulSet = { +const dummyStatefulSet: StatefulSet = { apiVersion: "v1", kind: "dummy", metadata: { @@ -88,6 +88,7 @@ const dummyStatefulSet = { getName: jest.fn(), getNs: jest.fn(), getAge: jest.fn(), + getTimeDiffFromNow: jest.fn(), getFinalizers: jest.fn(), getLabels: jest.fn(), getAnnotations: jest.fn(), diff --git a/src/renderer/components/table/table.tsx b/src/renderer/components/table/table.tsx index 9b5d396e85..f0a72c2eb6 100644 --- a/src/renderer/components/table/table.tsx +++ b/src/renderer/components/table/table.tsx @@ -16,6 +16,7 @@ export type TableSortBy = string; export type TableOrderBy = "asc" | "desc" | string; export type TableSortParams = { sortBy: TableSortBy; orderBy: TableOrderBy }; export type TableSortCallback = (data: D) => string | number | (string | number)[]; +export type TableSortCallbacks = { [columnId: string]: TableSortCallback }; export interface TableProps extends React.DOMAttributes { items?: ItemObject[]; // Raw items data @@ -24,11 +25,9 @@ export interface TableProps extends React.DOMAttributes { selectable?: boolean; // Highlight rows on hover scrollable?: boolean; // Use scrollbar if content is bigger than parent's height storageKey?: string; // Keep some data in localStorage & restore on page reload, e.g sorting params - sortable?: { - // Define sortable callbacks for every column in - // @sortItem argument in the callback is an object, provided in - [sortBy: string]: TableSortCallback; - }; + // Define sortable callbacks for every column in + // @sortItem argument in the callback is an object, provided in + sortable?: TableSortCallbacks; sortSyncWithUrl?: boolean; // sorting state is managed globally from url params sortByDefault?: Partial; // default sorting params onSort?: (params: TableSortParams) => void; // callback on sort change, default: global sync with url