diff --git a/src/renderer/api/kube-object.ts b/src/renderer/api/kube-object.ts index d16d65b6da..4ffd464d91 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 - getAge(humanize = true, compact = true, fromNow = false) { + getTimeDiffFromNow(): number { + return new Date().getTime() - new Date(this.metadata.creationTimestamp).getTime(); + } + + getAge(humanize = true, compact = true, fromNow = false): string | number { if (fromNow) { - return moment(this.metadata.creationTimestamp).fromNow(); + return moment(this.metadata.creationTimestamp).fromNow(); // "string", getTimeDiffFromNow() cannot be used } - 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.tsx b/src/renderer/components/+events/events.tsx index f385555aaa..4b7b64dd15 100644 --- a/src/renderer/components/+events/events.tsx +++ b/src/renderer/components/+events/events.tsx @@ -1,11 +1,15 @@ 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, TableProps } from "../table"; +import { IHeaderPlaceholders } from "../item-object-list"; import { Tooltip } from "../tooltip"; import { Link } from "react-router-dom"; import { cssNames, IClassName, stopPropagation } from "../../utils"; @@ -37,23 +41,79 @@ const defaultProps: Partial = { export class Events extends React.Component { static defaultProps = defaultProps as object; - get store() { + @observable sorting: TableSortParams = { + sortBy: columnId.age, + orderBy: "asc", + }; + + 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(), + }; + + private tableConfiguration: TableProps = { + sortSyncWithUrl: false, + sortByDefault: this.sorting, + onSort: params => this.sorting = params, + }; + + 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); } - render() { - const { store, items } = this; - const { compact, compactLimit, className, ...layoutProps } = this.props; - const visibleItems = compact ? items.slice(0, compactLimit) : items; + @computed get visibleItems(): KubeEvent[] { + const { compact, compactLimit } = this.props; + + if (compact) { + return this.items.slice(0, compactLimit); + } + + return this.items; + } + + customizeHeader = ({ info, title }: IHeaderPlaceholders) => { + const { compact } = this.props; + const { store, items, visibleItems } = this; const allEventsAreShown = visibleItems.length === items.length; - const compactModeHeader = <> - Events ({visibleItems.length} of {items.length}) - ; + // handle "compact"-mode header + if (compact) { + if (allEventsAreShown) return title; // title == "Events" + + return <> + {title} + ({visibleItems.length} of {items.length}) + ; + } + + return { + info: <> + {info} + + + }; + }; + + render() { + const { store, visibleItems } = this; + const { compact, compactLimit, className, ...layoutProps } = this.props; const events = ( { tableId="events" store={store} className={cssNames("Events", className, { compact })} + renderHeaderTitle="Events" + customizeHeader={this.customizeHeader} isSelectable={false} items={visibleItems} virtual={!compact} - renderHeaderTitle={compact && !allEventsAreShown ? compactModeHeader : "Events"} - tableProps={{ - sortSyncWithUrl: false, - sortByDefault: { - sortBy: columnId.age, - 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, - }} + tableProps={this.tableConfiguration} + sortingCallbacks={this.sortingCallbacks} searchFilters={[ (event: KubeEvent) => event.getSearchFields(), (event: KubeEvent) => event.message, (event: KubeEvent) => event.getSource(), (event: KubeEvent) => event.involvedObject.name, ]} - customizeHeader={({ title, info }) => ( - compact ? title : ({ - info: ( - <> - {info} - - - ) - }) - )} renderTableHeader={[ { title: "Type", className: "type", sortBy: columnId.type, id: columnId.type }, { title: "Message", className: "message", id: columnId.message }, 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/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx index 59bd18cd5e..895c275fbb 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -28,7 +28,7 @@ import { namespaceStore } from "../+namespaces/namespace.store"; export type SearchFilter = (item: T) => string | number | (string | number)[]; export type ItemsFilter = (items: T[]) => T[]; -interface IHeaderPlaceholders { +export interface IHeaderPlaceholders { title: ReactNode; search: ReactNode; filters: ReactNode; @@ -372,7 +372,7 @@ export class ItemListLayout extends React.Component { let header = this.renderHeaderContent(placeholders); if (customizeHeader) { - const modifiedHeader = customizeHeader(placeholders, header); + const modifiedHeader = customizeHeader(placeholders, header) ?? {}; if (isReactNode(modifiedHeader)) { header = modifiedHeader; diff --git a/src/renderer/components/table/table.tsx b/src/renderer/components/table/table.tsx index 9b5d396e85..19bd5b03e5 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,11 @@ 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