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

Fix: events sorting with compact=true is broken (#2141)

* fix: use default sorting for <Events/> as on "timeline" (fresh on top)

Signed-off-by: Roman <ixrock@gmail.com>

* responding to comments

Signed-off-by: Roman <ixrock@gmail.com>

* convert comments to jsdoc (table.tsx)

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2021-02-18 12:21:41 +02:00 committed by GitHub
parent 4d6cba4741
commit 5029196d15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 101 additions and 61 deletions

View File

@ -122,12 +122,15 @@ export class KubeObject implements ItemObject {
return this.metadata.namespace || undefined; return this.metadata.namespace || undefined;
} }
// todo: refactor with named arguments getTimeDiffFromNow(): number {
getAge(humanize = true, compact = true, fromNow = false) { return new Date().getTime() - new Date(this.metadata.creationTimestamp).getTime();
}
getAge(humanize = true, compact = true, fromNow = false): string | number {
if (fromNow) { 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) { if (humanize) {
return formatDuration(diff, compact); return formatDuration(diff, compact);

View File

@ -20,8 +20,8 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
protected sortItems(items: KubeEvent[]) { protected sortItems(items: KubeEvent[]) {
return super.sortItems(items, [ return super.sortItems(items, [
event => event.metadata.creationTimestamp event => event.getTimeDiffFromNow(), // keep events order as timeline ("fresh" on top)
], "desc"); ], "asc");
} }
getEventsByObject(obj: KubeObject): KubeEvent[] { getEventsByObject(obj: KubeObject): KubeEvent[] {

View File

@ -1,11 +1,15 @@
import "./events.scss"; import "./events.scss";
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import { computed, observable } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { orderBy } from "lodash";
import { TabLayout } from "../layout/tab-layout"; 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 { getDetailsUrl, KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
import { KubeEvent } from "../../api/endpoints/events.api"; import { KubeEvent } from "../../api/endpoints/events.api";
import { TableSortCallbacks, TableSortParams, TableProps } from "../table";
import { IHeaderPlaceholders } from "../item-object-list";
import { Tooltip } from "../tooltip"; import { Tooltip } from "../tooltip";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { cssNames, IClassName, stopPropagation } from "../../utils"; import { cssNames, IClassName, stopPropagation } from "../../utils";
@ -37,23 +41,79 @@ const defaultProps: Partial<Props> = {
export class Events extends React.Component<Props> { export class Events extends React.Component<Props> {
static defaultProps = defaultProps as object; 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; return eventStore;
} }
get items() { @computed get items(): KubeEvent[] {
return eventStore.contextItems; 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() { @computed get visibleItems(): KubeEvent[] {
const { store, items } = this; const { compact, compactLimit } = this.props;
const { compact, compactLimit, className, ...layoutProps } = this.props;
const visibleItems = compact ? items.slice(0, compactLimit) : items; 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 allEventsAreShown = visibleItems.length === items.length;
const compactModeHeader = <> // handle "compact"-mode header
Events <small>({visibleItems.length} of <Link to={eventsURL()}>{items.length}</Link>)</small> if (compact) {
</>; if (allEventsAreShown) return title; // title == "Events"
return <>
{title}
<span> ({visibleItems.length} of <Link to={eventsURL()}>{items.length}</Link>)</span>
</>;
}
return {
info: <>
{info}
<Icon
small
material="help_outline"
className="help-icon"
tooltip={`Limited to ${store.limit}`}
/>
</>
};
};
render() {
const { store, visibleItems } = this;
const { compact, compactLimit, className, ...layoutProps } = this.props;
const events = ( const events = (
<KubeObjectListLayout <KubeObjectListLayout
@ -62,45 +122,19 @@ export class Events extends React.Component<Props> {
tableId="events" tableId="events"
store={store} store={store}
className={cssNames("Events", className, { compact })} className={cssNames("Events", className, { compact })}
renderHeaderTitle="Events"
customizeHeader={this.customizeHeader}
isSelectable={false} isSelectable={false}
items={visibleItems} items={visibleItems}
virtual={!compact} virtual={!compact}
renderHeaderTitle={compact && !allEventsAreShown ? compactModeHeader : "Events"} tableProps={this.tableConfiguration}
tableProps={{ sortingCallbacks={this.sortingCallbacks}
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,
}}
searchFilters={[ searchFilters={[
(event: KubeEvent) => event.getSearchFields(), (event: KubeEvent) => event.getSearchFields(),
(event: KubeEvent) => event.message, (event: KubeEvent) => event.message,
(event: KubeEvent) => event.getSource(), (event: KubeEvent) => event.getSource(),
(event: KubeEvent) => event.involvedObject.name, (event: KubeEvent) => event.involvedObject.name,
]} ]}
customizeHeader={({ title, info }) => (
compact ? title : ({
info: (
<>
{info}
<Icon
small
material="help_outline"
className="help-icon"
tooltip={`Limited to ${store.limit}`}
/>
</>
)
})
)}
renderTableHeader={[ renderTableHeader={[
{ title: "Type", className: "type", sortBy: columnId.type, id: columnId.type }, { title: "Type", className: "type", sortBy: columnId.type, id: columnId.type },
{ title: "Message", className: "message", id: columnId.message }, { title: "Message", className: "message", id: columnId.message },

View File

@ -4,9 +4,9 @@ import "@testing-library/jest-dom/extend-expect";
import { DeploymentScaleDialog } from "./deployment-scale-dialog"; import { DeploymentScaleDialog } from "./deployment-scale-dialog";
jest.mock("../../api/endpoints"); jest.mock("../../api/endpoints");
import { deploymentApi } from "../../api/endpoints"; import { Deployment, deploymentApi } from "../../api/endpoints";
const dummyDeployment = { const dummyDeployment: Deployment = {
apiVersion: "v1", apiVersion: "v1",
kind: "dummy", kind: "dummy",
metadata: { metadata: {
@ -83,6 +83,7 @@ const dummyDeployment = {
getName: jest.fn(), getName: jest.fn(),
getNs: jest.fn(), getNs: jest.fn(),
getAge: jest.fn(), getAge: jest.fn(),
getTimeDiffFromNow: jest.fn(),
getFinalizers: jest.fn(), getFinalizers: jest.fn(),
getLabels: jest.fn(), getLabels: jest.fn(),
getAnnotations: jest.fn(), getAnnotations: jest.fn(),

View File

@ -4,9 +4,9 @@ jest.mock("../../api/endpoints");
import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog"; import { ReplicaSetScaleDialog } from "./replicaset-scale-dialog";
import { render, waitFor, fireEvent } from "@testing-library/react"; import { render, waitFor, fireEvent } from "@testing-library/react";
import React from "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", apiVersion: "v1",
kind: "dummy", kind: "dummy",
metadata: { metadata: {
@ -67,7 +67,6 @@ const dummyReplicaSet = {
getCurrent: jest.fn(), getCurrent: jest.fn(),
getReady: jest.fn(), getReady: jest.fn(),
getImages: jest.fn(), getImages: jest.fn(),
getReplicas: jest.fn(),
getSelectors: jest.fn(), getSelectors: jest.fn(),
getTemplateLabels: jest.fn(), getTemplateLabels: jest.fn(),
getAffinity: jest.fn(), getAffinity: jest.fn(),
@ -79,6 +78,7 @@ const dummyReplicaSet = {
getName: jest.fn(), getName: jest.fn(),
getNs: jest.fn(), getNs: jest.fn(),
getAge: jest.fn(), getAge: jest.fn(),
getTimeDiffFromNow: jest.fn(),
getFinalizers: jest.fn(), getFinalizers: jest.fn(),
getLabels: jest.fn(), getLabels: jest.fn(),
getAnnotations: jest.fn(), getAnnotations: jest.fn(),

View File

@ -1,12 +1,12 @@
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
jest.mock("../../api/endpoints"); jest.mock("../../api/endpoints");
import { statefulSetApi } from "../../api/endpoints"; import { StatefulSet, statefulSetApi } from "../../api/endpoints";
import { StatefulSetScaleDialog } from "./statefulset-scale-dialog"; import { StatefulSetScaleDialog } from "./statefulset-scale-dialog";
import { render, waitFor, fireEvent } from "@testing-library/react"; import { render, waitFor, fireEvent } from "@testing-library/react";
import React from "react"; import React from "react";
const dummyStatefulSet = { const dummyStatefulSet: StatefulSet = {
apiVersion: "v1", apiVersion: "v1",
kind: "dummy", kind: "dummy",
metadata: { metadata: {
@ -88,6 +88,7 @@ const dummyStatefulSet = {
getName: jest.fn(), getName: jest.fn(),
getNs: jest.fn(), getNs: jest.fn(),
getAge: jest.fn(), getAge: jest.fn(),
getTimeDiffFromNow: jest.fn(),
getFinalizers: jest.fn(), getFinalizers: jest.fn(),
getLabels: jest.fn(), getLabels: jest.fn(),
getAnnotations: jest.fn(), getAnnotations: jest.fn(),

View File

@ -28,7 +28,7 @@ import { namespaceStore } from "../+namespaces/namespace.store";
export type SearchFilter<T extends ItemObject = any> = (item: T) => string | number | (string | number)[]; export type SearchFilter<T extends ItemObject = any> = (item: T) => string | number | (string | number)[];
export type ItemsFilter<T extends ItemObject = any> = (items: T[]) => T[]; export type ItemsFilter<T extends ItemObject = any> = (items: T[]) => T[];
interface IHeaderPlaceholders { export interface IHeaderPlaceholders {
title: ReactNode; title: ReactNode;
search: ReactNode; search: ReactNode;
filters: ReactNode; filters: ReactNode;
@ -372,7 +372,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
let header = this.renderHeaderContent(placeholders); let header = this.renderHeaderContent(placeholders);
if (customizeHeader) { if (customizeHeader) {
const modifiedHeader = customizeHeader(placeholders, header); const modifiedHeader = customizeHeader(placeholders, header) ?? {};
if (isReactNode(modifiedHeader)) { if (isReactNode(modifiedHeader)) {
header = modifiedHeader; header = modifiedHeader;

View File

@ -16,6 +16,7 @@ export type TableSortBy = string;
export type TableOrderBy = "asc" | "desc" | string; export type TableOrderBy = "asc" | "desc" | string;
export type TableSortParams = { sortBy: TableSortBy; orderBy: TableOrderBy }; export type TableSortParams = { sortBy: TableSortBy; orderBy: TableOrderBy };
export type TableSortCallback<D = any> = (data: D) => string | number | (string | number)[]; export type TableSortCallback<D = any> = (data: D) => string | number | (string | number)[];
export type TableSortCallbacks = { [columnId: string]: TableSortCallback };
export interface TableProps extends React.DOMAttributes<HTMLDivElement> { export interface TableProps extends React.DOMAttributes<HTMLDivElement> {
items?: ItemObject[]; // Raw items data items?: ItemObject[]; // Raw items data
@ -24,11 +25,11 @@ export interface TableProps extends React.DOMAttributes<HTMLDivElement> {
selectable?: boolean; // Highlight rows on hover selectable?: boolean; // Highlight rows on hover
scrollable?: boolean; // Use scrollbar if content is bigger than parent's height 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 storageKey?: string; // Keep some data in localStorage & restore on page reload, e.g sorting params
sortable?: { /**
// Define sortable callbacks for every column in <TableHead><TableCell sortBy="someCol"><TableHead> * Define sortable callbacks for every column in <TableHead><TableCell sortBy="someCol"><TableHead>
// @sortItem argument in the callback is an object, provided in <TableRow sortItem={someColDataItem}/> * @sortItem argument in the callback is an object, provided in <TableRow sortItem={someColDataItem}/>
[sortBy: string]: TableSortCallback; */
}; sortable?: TableSortCallbacks;
sortSyncWithUrl?: boolean; // sorting state is managed globally from url params sortSyncWithUrl?: boolean; // sorting state is managed globally from url params
sortByDefault?: Partial<TableSortParams>; // default sorting params sortByDefault?: Partial<TableSortParams>; // default sorting params
onSort?: (params: TableSortParams) => void; // callback on sort change, default: global sync with url onSort?: (params: TableSortParams) => void; // callback on sort change, default: global sync with url