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:
parent
4d6cba4741
commit
5029196d15
@ -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);
|
||||||
|
|||||||
@ -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[] {
|
||||||
|
|||||||
@ -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 },
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user