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

List layout column injection token package (#7544)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>
Co-authored-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Sami Tiilikainen 2023-04-20 16:01:50 +03:00 committed by GitHub
parent 4bb37df1cd
commit 5eb41e8a6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 3589 additions and 196 deletions

19
package-lock.json generated
View File

@ -3935,6 +3935,10 @@
"resolved": "packages/technical-features/application/legacy-extensions",
"link": true
},
"node_modules/@k8slens/list-layout": {
"resolved": "packages/list-layout",
"link": true
},
"node_modules/@k8slens/messaging": {
"resolved": "packages/technical-features/messaging/agnostic",
"link": true
@ -35719,6 +35723,7 @@
"@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^9.0.1",
"@hapi/subtext": "^7.1.0",
"@k8slens/list-layout": "^1.0.0-alpha.0",
"@k8slens/metrics": "^6.5.0-alpha.3",
"@k8slens/node-fetch": "^6.5.0-alpha.3",
"@k8slens/react-application": "^1.0.0-alpha.2",
@ -36657,6 +36662,20 @@
}
}
},
"packages/list-layout": {
"name": "@k8slens/list-layout",
"version": "1.0.0-alpha.0",
"license": "MIT",
"devDependencies": {
"@k8slens/eslint-config": "^6.5.0-alpha.2",
"@k8slens/jest": "^6.5.0-alpha.2",
"@k8slens/typescript": "^6.5.0-alpha.2"
},
"peerDependencies": {
"@ogre-tools/injectable": "^15.1.2",
"react": "^17.0.2"
}
},
"packages/metrics": {
"name": "@k8slens/metrics",
"version": "6.5.0-alpha.3",

View File

@ -115,6 +115,7 @@
"@k8slens/metrics": "^6.5.0-alpha.3",
"@k8slens/node-fetch": "^6.5.0-alpha.3",
"@k8slens/react-application": "^1.0.0-alpha.2",
"@k8slens/list-layout": "^1.0.0-alpha.0",
"@kubernetes/client-node": "^0.18.1",
"@material-ui/styles": "^4.11.5",
"@sentry/electron": "^3.0.8",

View File

@ -3,14 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ItemObject } from "@k8slens/list-layout";
import autoBind from "auto-bind";
import orderBy from "lodash/orderBy";
import { action, computed, observable, when, makeObservable } from "mobx";
export interface ItemObject {
getId(): string;
getName(): string;
}
export abstract class ItemStore<Item extends ItemObject> {
protected defaultSorting = (item: Item) => item.getName();

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ItemObject } from "../../item.store";
import type { ItemObject } from "@k8slens/list-layout";
import type { HelmReleaseDetails } from "./helm-releases.api/request-details.injectable";
export interface HelmReleaseUpdateDetails {

View File

@ -22,7 +22,7 @@ import {
isTypedArray,
isRecord,
} from "@k8slens/utilities";
import type { ItemObject } from "../item.store";
import type { ItemObject } from "@k8slens/list-layout";
import type { Patch } from "rfc6902";
import assert from "assert";
import type { JsonObject } from "type-fest";

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,132 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import navigateToPodsInjectable from "../../../common/front-end-routing/routes/cluster/workloads/pods/navigate-to-pods.injectable";
import { type ApplicationBuilder, getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import podStoreInjectable from "../../../renderer/components/+workloads-pods/store.injectable";
import type { PodMetrics } from "../../../common/k8s-api/endpoints";
import { Pod } from "../../../common/k8s-api/endpoints";
import podMetricsApiInjectable from "../../../common/k8s-api/endpoints/pod-metrics.api.injectable";
import requestMetricsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-metrics.injectable";
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
describe("workloads / pods", () => {
let rendered: RenderResult;
let applicationBuilder: ApplicationBuilder;
const podMetrics: PodMetrics[] = [];
beforeEach(async () => {
applicationBuilder = getApplicationBuilder().setEnvironmentToClusterFrame();
applicationBuilder.namespaces.add("default");
applicationBuilder.beforeWindowStart(({ windowDi }) => {
applicationBuilder.allowKubeResource({
apiName: "pods",
group: "",
});
windowDi.override(podMetricsApiInjectable, () => ({
list: async () => podMetrics,
} as any));
const apiManager = windowDi.inject(apiManagerInjectable);
const podStore = windowDi.inject(podStoreInjectable);
apiManager.registerStore(podStore);
});
});
describe("when navigating to workloads / pods view", () => {
describe("given pods are loading", () => {
beforeEach(async () => {
applicationBuilder.afterWindowStart(({ windowDi }) => {
const podStore = windowDi.inject(podStoreInjectable);
podStore.items.clear();
podStore.isLoaded = false;
podStore.isLoading = true;
});
rendered = await applicationBuilder.render();
applicationBuilder.navigateWith(navigateToPodsInjectable);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows loading spinner", async () => {
expect(await rendered.findByTestId("kube-object-list-layout-spinner")).toBeInTheDocument();
});
});
describe("given no pods", () => {
beforeEach(async () => {
applicationBuilder.afterWindowStart(({ windowDi }) => {
const podStore = windowDi.inject(podStoreInjectable);
podStore.items.clear();
podStore.isLoaded = true;
});
rendered = await applicationBuilder.render();
applicationBuilder.navigateWith(navigateToPodsInjectable);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows item list is empty", async () => {
expect(rendered.getByText("Item list is empty")).toBeInTheDocument();
});
});
describe("given a namespace has pods", () => {
beforeEach(async () => {
applicationBuilder.afterWindowStart(({ windowDi }) => {
windowDi.override(requestMetricsInjectable, () => () => ({} as any));
const podStore = windowDi.inject(podStoreInjectable);
podStore.items.push(new Pod({
apiVersion: "v1",
kind: "Pod",
metadata: {
name: "test-pod-1",
namespace: "default",
resourceVersion: "irrelevant",
selfLink: "/api/v1/namespaces/default/pods/test-pod-1",
uid: "uuid-1",
},
spec: {
containers: [
{
name: "container-1",
},
{
name: "container-2",
},
],
},
status: {} as any,
}));
podStore.isLoaded = true;
});
rendered = await applicationBuilder.render();
applicationBuilder.navigateWith(navigateToPodsInjectable);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("renders the pod list", async () => {
expect(await rendered.findByTestId(`list-pod-name-uuid-1`)).toBeInTheDocument();
});
});
});
});

View File

@ -13,7 +13,7 @@ import { Icon } from "../icon";
import { SubHeader } from "../layout/sub-header";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { cssNames, prevDefault } from "@k8slens/utilities";
import type { ItemObject } from "../../../common/item.store";
import type { ItemObject } from "@k8slens/list-layout";
import { Spinner } from "../spinner";
import type { ApiManager } from "../../../common/k8s-api/api-manager";
import { KubeObjectAge } from "../kube-object/age";

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import React from "react";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import type { KubeObjectListLayoutColumn } from "@k8slens/list-layout";
import { kubeObjectListLayoutColumnInjectionToken } from "@k8slens/list-layout";
import { KubeObjectAge } from "../../kube-object/age";
export const podsAgeColumnInjectable = getInjectable({
id: "pods-age-column",
instantiate: (): KubeObjectListLayoutColumn<Pod> => {
const columnId = "age";
return {
id: columnId,
kind: "Pod",
apiVersion: "v1",
priority: 30,
content: (pod: Pod) => {
return <KubeObjectAge key="age" object={pod} />;
},
header: { title: "Age", className: "age", sortBy: columnId, id: columnId },
sortingCallBack: (pod: Pod) => -pod.getCreationTimestamp(),
};
},
injectionToken: kubeObjectListLayoutColumnInjectionToken,
});

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { cssNames } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import startCase from "lodash/startCase";
import React from "react";
import type { ContainerStateValues, Pod } from "../../../../common/k8s-api/endpoints";
import type { KubeObjectListLayoutColumn } from "@k8slens/list-layout";
import { kubeObjectListLayoutColumnInjectionToken } from "@k8slens/list-layout";
import { StatusBrick } from "../../status-brick";
function renderState(name: string, ready: boolean, key: string, data?: ContainerStateValues) {
if (!data) {
return;
}
return (
<>
<div className="title">
{name}
{" "}
<span className="text-secondary">
{key}
{ready ? ", ready" : ""}
</span>
</div>
{Object.entries(data).map(([name, value]) => (
<React.Fragment key={name}>
<div className="name">{startCase(name)}</div>
<div className="value">{value}</div>
</React.Fragment>
))}
</>
);
}
function renderContainersStatus(pod: Pod) {
return pod.getContainerStatuses().map(({ name, state, ready }) => {
return (
<StatusBrick
key={name}
className={cssNames(state, { ready })}
tooltip={{
formatters: {
tableView: true,
nowrap: true,
},
children: (
<>
{renderState(name, ready, "running", state?.running)}
{renderState(name, ready, "waiting", state?.waiting)}
{renderState(name, ready, "terminated", state?.terminated)}
</>
),
}}
/>
);
});
}
export const podsContainersColumnInjectable = getInjectable({
id: "pods-containers-column",
instantiate: (): KubeObjectListLayoutColumn<Pod> => {
const columnId = "containers";
return {
id: columnId,
kind: "Pod",
apiVersion: "v1",
priority: 80,
content: (pod: Pod) => {
return renderContainersStatus(pod);
},
header: {
title: "Containers",
className: "containers",
sortBy: columnId,
id: columnId,
},
sortingCallBack: (pod: Pod) => pod.getContainerStatuses().length,
};
},
injectionToken: kubeObjectListLayoutColumnInjectionToken,
});

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getConvertedParts } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import React from "react";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import type { KubeObjectListLayoutColumn } from "@k8slens/list-layout";
import { kubeObjectListLayoutColumnInjectionToken } from "@k8slens/list-layout";
import { Tooltip } from "@k8slens/tooltip";
export const podsNameColumnInjectable = getInjectable({
id: "pods-name-column",
instantiate: (): KubeObjectListLayoutColumn<Pod> => {
const columnId = "name";
return {
id: columnId,
kind: "Pod",
apiVersion: "v1",
priority: 100,
content: (pod: Pod) => {
return (
<>
<span id={`list-pod-name-${pod.getId()}`} data-testid={`list-pod-name-${pod.getId()}`}>
{pod.getName()}
</span>
<Tooltip targetId={`list-pod-name-${pod.getId()}`}>
{pod.getName()}
</Tooltip>
</>
);
},
header: { title: "Name", className: "name", sortBy: columnId, id: columnId },
sortingCallBack: (pod: Pod) => getConvertedParts(pod.getName()),
searchFilter: (pod: Pod) => pod.getSearchFields(),
};
},
injectionToken: kubeObjectListLayoutColumnInjectionToken,
});

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import React from "react";
import { NamespaceSelectBadge } from "../../+namespaces/namespace-select-badge";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import type { KubeObjectListLayoutColumn } from "@k8slens/list-layout";
import { kubeObjectListLayoutColumnInjectionToken } from "@k8slens/list-layout";
export const podsNamespaceColumnInjectable = getInjectable({
id: "pods-namespace-column",
instantiate: (): KubeObjectListLayoutColumn<Pod> => {
const columnId = "namespace";
return {
id: columnId,
kind: "Pod",
apiVersion: "v1",
priority: 90,
content: (pod: Pod) => {
return (<NamespaceSelectBadge key="namespace" namespace={pod.getNs()} />);
},
header: { title: "Namespace", className: "namespace", sortBy: columnId, id: columnId },
sortingCallBack: (pod: Pod) => pod.getNs(),
};
},
injectionToken: kubeObjectListLayoutColumnInjectionToken,
});

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import React from "react";
import { Link } from "react-router-dom";
import nodeApiInjectable from "../../../../common/k8s-api/endpoints/node.api.injectable";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import { Badge } from "../../badge";
import getDetailsUrlInjectable from "../../kube-detail-params/get-details-url.injectable";
import type { KubeObjectListLayoutColumn } from "@k8slens/list-layout";
import { kubeObjectListLayoutColumnInjectionToken } from "@k8slens/list-layout";
import { stopPropagation } from "@k8slens/utilities";
export const podsNodeColumnInjectable = getInjectable({
id: "pods-node-column",
instantiate: (di): KubeObjectListLayoutColumn<Pod> => {
const getDetailsUrl = di.inject(getDetailsUrlInjectable);
const nodeApi = di.inject(nodeApiInjectable);
const columnId = "node";
return {
id: columnId,
kind: "Pod",
apiVersion: "v1",
priority: 50,
content: (pod: Pod) => {
return pod.getNodeName() ? (
<Badge
flat
key="node"
className="node"
tooltip={pod.getNodeName()}
expandable={false}
>
<Link
to={getDetailsUrl(nodeApi.getUrl({ name: pod.getNodeName() }))}
onClick={stopPropagation}>
{pod.getNodeName()}
</Link>
</Badge>
)
: "";
},
header: { title: "Node", className: "node", sortBy: columnId, id: columnId },
sortingCallBack: (pod: Pod) => pod.getNodeName(),
};
},
injectionToken: kubeObjectListLayoutColumnInjectionToken,
});

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { stopPropagation } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import React from "react";
import { Link } from "react-router-dom";
import apiManagerInjectable from "../../../../common/k8s-api/api-manager/manager.injectable";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import { Badge } from "../../badge";
import getDetailsUrlInjectable from "../../kube-detail-params/get-details-url.injectable";
import type { KubeObjectListLayoutColumn } from "@k8slens/list-layout";
import { kubeObjectListLayoutColumnInjectionToken } from "@k8slens/list-layout";
export const podsOwnersColumnInjectable = getInjectable({
id: "pods-owners-column",
instantiate: (di): KubeObjectListLayoutColumn<Pod> => {
const getDetailsUrl = di.inject(getDetailsUrlInjectable);
const apiManager = di.inject(apiManagerInjectable);
const columnId = "owners";
return {
id: columnId,
kind: "Pod",
apiVersion: "v1",
priority: 60,
content: (pod: Pod) => {
return pod.getOwnerRefs().map(ref => {
const { kind, name } = ref;
const detailsLink = getDetailsUrl(apiManager.lookupApiLink(ref, pod));
return (
<Badge
flat
key={name}
className="owner"
tooltip={name}
>
<Link to={detailsLink} onClick={stopPropagation}>
{kind}
</Link>
</Badge>
);
});
},
header: { title: "Controlled By", className: "owners", sortBy: columnId, id: columnId },
sortingCallBack: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind),
};
},
injectionToken: kubeObjectListLayoutColumnInjectionToken,
});

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import type { KubeObjectListLayoutColumn } from "@k8slens/list-layout";
import { kubeObjectListLayoutColumnInjectionToken } from "@k8slens/list-layout";
export const podsQosColumnInjectable = getInjectable({
id: "pods-qos-column",
instantiate: (): KubeObjectListLayoutColumn<Pod> => {
const columnId = "qos";
return {
id: columnId,
kind: "Pod",
apiVersion: "v1",
priority: 40,
content: (pod: Pod) => {
return pod.getQosClass();
},
header: { title: "QoS", className: "qos", sortBy: columnId, id: columnId },
sortingCallBack: (pod: Pod) => pod.getQosClass(),
};
},
injectionToken: kubeObjectListLayoutColumnInjectionToken,
});

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import type { KubeObjectListLayoutColumn } from "@k8slens/list-layout";
import { kubeObjectListLayoutColumnInjectionToken } from "@k8slens/list-layout";
export const podsRestartsColumnInjectable = getInjectable({
id: "pods-restarts-column",
instantiate: (): KubeObjectListLayoutColumn<Pod> => {
const columnId = "restarts";
return {
id: columnId,
kind: "Pod",
apiVersion: "v1",
priority: 70,
content: (pod: Pod) => {
return pod.getRestartsCount();
},
header: { title: "Restarts", className: "restarts", sortBy: columnId, id: columnId },
sortingCallBack: (pod: Pod) => pod.getRestartsCount(),
};
},
injectionToken: kubeObjectListLayoutColumnInjectionToken,
});

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { kebabCase } from "lodash";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import type { KubeObjectListLayoutColumn } from "@k8slens/list-layout";
import { kubeObjectListLayoutColumnInjectionToken } from "@k8slens/list-layout";
export const podsStatusColumnInjectable = getInjectable({
id: "pods-status-column",
instantiate: (): KubeObjectListLayoutColumn<Pod> => {
const columnId = "status";
return {
id: columnId,
kind: "Pod",
apiVersion: "v1",
priority: 0,
content: (pod: Pod) => {
return { title: pod.getStatusMessage(), className: kebabCase(pod.getStatusMessage()) };
},
header: { title: "Status", className: "status", sortBy: columnId, id: columnId },
sortingCallBack: (pod: Pod) => pod.getStatusMessage(),
searchFilter: (pod: Pod) => pod.getStatusMessage(),
};
},
injectionToken: kubeObjectListLayoutColumnInjectionToken,
});

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import React from "react";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import type { KubeObjectListLayoutColumn } from "@k8slens/list-layout";
import { kubeObjectListLayoutColumnInjectionToken } from "@k8slens/list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
export const podsQosColumnInjectable = getInjectable({
id: "pods-status-icon-column",
instantiate: (): KubeObjectListLayoutColumn<Pod> => {
const columnId = "qos";
return {
id: columnId,
kind: "Pod",
apiVersion: "v1",
priority: 99,
content: (pod: Pod) => {
return <KubeObjectStatusIcon key="icon" object={pod} />;
},
header: { className: "warning", showWithColumn: "name" },
sortingCallBack: (pod: Pod) => pod.getQosClass(),
};
},
injectionToken: kubeObjectListLayoutColumnInjectionToken,
});

View File

@ -7,103 +7,23 @@ import "./pods.scss";
import React from "react";
import { observer } from "mobx-react";
import { Link } from "react-router-dom";
import { KubeObjectListLayout } from "../kube-object-list-layout";
import type { ContainerStateValues, NodeApi, Pod } from "../../../common/k8s-api/endpoints";
import { StatusBrick } from "../status-brick";
import { cssNames, getConvertedParts, object, stopPropagation } from "@k8slens/utilities";
import startCase from "lodash/startCase";
import kebabCase from "lodash/kebabCase";
import type { ApiManager } from "../../../common/k8s-api/api-manager";
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
import { Badge } from "../badge";
import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout";
import { KubeObjectAge } from "../kube-object/age";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { GetDetailsUrl } from "../kube-detail-params/get-details-url.injectable";
import getDetailsUrlInjectable from "../kube-detail-params/get-details-url.injectable";
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
import type { EventStore } from "../+events/store";
import type { PodStore } from "./store";
import nodeApiInjectable from "../../../common/k8s-api/endpoints/node.api.injectable";
import eventStoreInjectable from "../+events/store.injectable";
import podStoreInjectable from "./store.injectable";
import { NamespaceSelectBadge } from "../+namespaces/namespace-select-badge";
import { Tooltip } from "@k8slens/tooltip";
enum columnId {
name = "name",
namespace = "namespace",
containers = "containers",
restarts = "restarts",
age = "age",
qos = "qos",
node = "node",
owners = "owners",
status = "status",
}
interface Dependencies {
getDetailsUrl: GetDetailsUrl;
apiManager: ApiManager;
eventStore: EventStore;
podStore: PodStore;
nodeApi: NodeApi;
}
@observer
class NonInjectedPods extends React.Component<Dependencies> {
renderState(name: string, ready: boolean, key: string, data?: ContainerStateValues) {
if (!data) {
return;
}
return (
<>
<div className="title">
{name}
{" "}
<span className="text-secondary">
{key}
{ready ? ", ready" : ""}
</span>
</div>
{object.entries(data).map(([name, value]) => (
<React.Fragment key={name}>
<div className="name">{startCase(name)}</div>
<div className="value">{value}</div>
</React.Fragment>
))}
</>
);
}
renderContainersStatus(pod: Pod) {
return pod.getContainerStatuses().map(({ name, state, ready }) => {
return (
<StatusBrick
key={name}
className={cssNames(state, { ready })}
tooltip={{
formatters: {
tableView: true,
nowrap: true,
},
children: (
<>
{this.renderState(name, ready, "running", state?.running)}
{this.renderState(name, ready, "waiting", state?.waiting)}
{this.renderState(name, ready, "terminated", state?.terminated)}
</>
),
}}
/>
);
});
}
render() {
const { apiManager, getDetailsUrl, podStore, eventStore, nodeApi } = this.props;
const { podStore, eventStore } = this.props;
return (
<SiblingsInTabLayout>
@ -113,109 +33,12 @@ class NonInjectedPods extends React.Component<Dependencies> {
dependentStores={[eventStore]} // status icon component uses event store
tableId="workloads_pods"
isConfigurable
sortingCallbacks={{
[columnId.name]: pod => getConvertedParts(pod.getName()),
[columnId.namespace]: pod => pod.getNs(),
[columnId.containers]: pod => pod.getContainerStatuses().length,
[columnId.restarts]: pod => pod.getRestartsCount(),
[columnId.owners]: pod => pod.getOwnerRefs().map(ref => ref.kind),
[columnId.qos]: pod => pod.getQosClass(),
[columnId.node]: pod => pod.getNodeName(),
[columnId.age]: pod => -pod.getCreationTimestamp(),
[columnId.status]: pod => pod.getStatusMessage(),
}}
searchFilters={[
pod => pod.getSearchFields(),
pod => pod.getStatusMessage(),
pod => pod.status?.podIP,
pod => pod.getNodeName(),
]}
renderHeaderTitle="Pods"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ className: "warning", showWithColumn: columnId.name },
{
title: "Namespace",
className: "namespace",
sortBy: columnId.namespace,
id: columnId.namespace,
},
{
title: "Containers",
className: "containers",
sortBy: columnId.containers,
id: columnId.containers,
},
{
title: "Restarts",
className: "restarts",
sortBy: columnId.restarts,
id: columnId.restarts,
},
{
title: "Controlled By",
className: "owners",
sortBy: columnId.owners,
id: columnId.owners,
},
{ title: "Node", className: "node", sortBy: columnId.node, id: columnId.node },
{ title: "QoS", className: "qos", sortBy: columnId.qos, id: columnId.qos },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
]}
renderTableContents={pod => [
<>
<span id={`list-pod-${pod.getId()}`}>
{pod.getName()}
</span>
<Tooltip targetId={`list-pod-${pod.getId()}`}>
{pod.getName()}
</Tooltip>
</>,
<KubeObjectStatusIcon key="icon" object={pod} />,
<NamespaceSelectBadge
key="namespace"
namespace={pod.getNs()}
/>,
this.renderContainersStatus(pod),
pod.getRestartsCount(),
pod.getOwnerRefs().map(ref => {
const { kind, name } = ref;
const detailsLink = getDetailsUrl(apiManager.lookupApiLink(ref, pod));
return (
<Badge
flat
key={name}
className="owner"
tooltip={name}
>
<Link to={detailsLink} onClick={stopPropagation}>
{kind}
</Link>
</Badge>
);
}),
pod.getNodeName() ? (
<Badge
flat
key="node"
className="node"
tooltip={pod.getNodeName()}
expandable={false}
>
<Link
to={getDetailsUrl(nodeApi.getUrl({ name: pod.getNodeName() }))}
onClick={stopPropagation}>
{pod.getNodeName()}
</Link>
</Badge>
)
: "",
pod.getQosClass(),
<KubeObjectAge key="age" object={pod} />,
{ title: pod.getStatusMessage(), className: kebabCase(pod.getStatusMessage()) },
]}
renderTableHeader={[]}
renderTableContents={() => []}
/>
</SiblingsInTabLayout>
);
@ -225,9 +48,6 @@ class NonInjectedPods extends React.Component<Dependencies> {
export const Pods = withInjectables<Dependencies>(NonInjectedPods, {
getProps: (di, props) => ({
...props,
apiManager: di.inject(apiManagerInjectable),
getDetailsUrl: di.inject(getDetailsUrlInjectable),
nodeApi: di.inject(nodeApiInjectable),
eventStore: di.inject(eventStoreInjectable),
podStore: di.inject(podStoreInjectable),
}),

View File

@ -19,7 +19,7 @@ import type { AddRemoveButtonsProps } from "../add-remove-buttons";
import { AddRemoveButtons } from "../add-remove-buttons";
import { NoItems } from "../no-items";
import { Spinner } from "../spinner";
import type { ItemObject } from "../../../common/item.store";
import type { ItemObject } from "@k8slens/list-layout";
import type { Filter, PageFiltersStore } from "./page-filters/store";
import type { LensTheme } from "../../themes/lens-theme";
import { MenuActions } from "../menu/menu-actions";

View File

@ -10,7 +10,7 @@ import React from "react";
import { observer } from "mobx-react";
import type { IClassName } from "@k8slens/utilities";
import { cssNames, isDefined } from "@k8slens/utilities";
import type { ItemObject } from "../../../common/item.store";
import type { ItemObject } from "@k8slens/list-layout";
import type { Filter } from "./page-filters/store";
import type { HeaderCustomizer, HeaderPlaceholders, ItemListStore, SearchFilter } from "./list-layout";
import { SearchInputUrl } from "../input";

View File

@ -14,7 +14,7 @@ import type { TableCellProps, TableProps, TableRowProps, TableSortCallbacks } fr
import type { IClassName, SingleOrMany } from "@k8slens/utilities";
import { cssNames, noop } from "@k8slens/utilities";
import type { AddRemoveButtonsProps } from "../add-remove-buttons";
import type { ItemObject } from "../../../common/item.store";
import type { ItemObject } from "@k8slens/list-layout";
import type { SearchInputUrlProps } from "../input";
import type { PageFiltersStore } from "./page-filters/store";
import { FilterType } from "./page-filters/store";

View File

@ -104,10 +104,236 @@ exports[`kube-object-list-layout given pod store renders 1`] = `
class="items box grow flex column"
>
<div
class="Table flex column KubeObjectListLayout Pods box grow dark selectable scrollable autoSize virtual"
class="Table flex column KubeObjectListLayout Pods box grow dark selectable scrollable sortable autoSize virtual"
>
<div
class="TableHead sticky nowrap topLine"
>
<div
class="TableCell checkbox"
>
<label
class="Checkbox flex align-center"
>
<input
type="checkbox"
/>
<i
class="box flex align-center"
/>
</label>
</div>
<div
class="TableCell name nowrap sorting"
id="name"
>
<div
class="content"
>
Name
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell warning nowrap"
>
<div
class="content"
/>
</div>
<div
class="TableCell namespace nowrap sorting"
id="namespace"
>
<div
class="content"
>
Namespace
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell containers nowrap sorting"
id="containers"
>
<div
class="content"
>
Containers
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell restarts nowrap sorting"
id="restarts"
>
<div
class="content"
>
Restarts
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell owners nowrap sorting"
id="owners"
>
<div
class="content"
>
Controlled By
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell node nowrap sorting"
id="node"
>
<div
class="content"
>
Node
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell qos nowrap sorting"
id="qos"
>
<div
class="content"
>
QoS
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell age nowrap sorting"
id="age"
>
<div
class="content"
>
Age
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell status nowrap sorting"
id="status"
>
<div
class="content"
>
Status
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell menu nowrap"
>
<div
class="content"
>
<i
class="Icon material interactive focusable"
id="menu-actions-for-item-object-list-content"
tabindex="0"
>
<span
class="icon"
data-icon-name="more_vert"
>
more_vert
</span>
</i>
</div>
</div>
</div>
<div
class="Spinner singleColor center"
data-testid="kube-object-list-layout-spinner"
/>
</div>
<div

View File

@ -21,6 +21,7 @@ import directoryForKubeConfigsInjectable from "../../../common/app-paths/directo
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import type { PodStore } from "../+workloads-pods/store";
import { Cluster } from "../../../common/cluster/cluster";
import isTableColumnHiddenInjectable from "../../../features/user-preferences/common/is-table-column-hidden.injectable";
describe("kube-object-list-layout", () => {
let di: DiContainer;
@ -33,6 +34,7 @@ describe("kube-object-list-layout", () => {
di.override(directoryForUserDataInjectable, () => "/some-user-store-path");
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
di.override(isTableColumnHiddenInjectable, () => () => false);
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",

View File

@ -29,6 +29,9 @@ import type { ToggleKubeDetailsPane } from "../kube-detail-params/toggle-details
import kubeSelectedUrlParamInjectable from "../kube-detail-params/kube-selected-url.injectable";
import toggleKubeDetailsPaneInjectable from "../kube-detail-params/toggle-details.injectable";
import type { ClusterContext } from "../../cluster-frame-context/cluster-frame-context";
import type { KubeObjectListLayoutColumn, ItemObject } from "@k8slens/list-layout";
import { kubeObjectListLayoutColumnInjectionToken } from "@k8slens/list-layout";
import { sortBy } from "lodash";
export interface KubeObjectListLayoutProps<
K extends KubeObject,
@ -53,6 +56,7 @@ interface Dependencies {
subscribeToStores: SubscribeStores;
kubeSelectedUrlParam: PageParam<string>;
toggleKubeDetailsPane: ToggleKubeDetailsPane;
columns: KubeObjectListLayoutColumn<ItemObject>[];
}
const getLoadErrorMessage = (error: unknown): string => {
@ -140,9 +144,25 @@ class NonInjectedKubeObjectListLayout<
dependentStores,
toggleKubeDetailsPane: toggleDetails,
onDetails,
renderTableContents,
renderTableHeader,
columns,
sortingCallbacks = {},
...layoutProps
} = this.props;
const resourceName = this.props.resourceName || ResourceNames[ResourceKindMap[store.api.kind]] || store.api.kind;
const targetColumns = columns.filter((col) => col.kind === store.api.kind && col.apiVersion === store.api.apiVersionWithGroup);
targetColumns.forEach((col) => {
if (col.sortingCallBack) {
sortingCallbacks[col.id] = col.sortingCallBack;
}
});
const headers = sortBy([
...(renderTableHeader || []).map((header, index) => ({ priority: (20 - index), header })),
...targetColumns,
], (v) => -v.priority).map((col) => col.header);
return (
<ItemListLayout<K, false>
@ -175,6 +195,15 @@ class NonInjectedKubeObjectListLayout<
]}
renderItemMenu={item => <KubeObjectMenu object={item} />}
onDetails={onDetails ?? ((item) => toggleDetails(item.selfLink))}
sortingCallbacks={sortingCallbacks}
renderTableHeader={headers}
renderTableContents={(item) => {
return sortBy([
...(renderTableContents(item).map((content, index) => ({ priority: (20 - index), content }))),
...targetColumns.map((col) => ({ priority: col.priority, content: col.content(item) })),
], (item) => -item.priority).map((value) => value.content);
}}
spinnerTestId="kube-object-list-layout-spinner"
{...layoutProps}
/>
);
@ -191,6 +220,7 @@ export const KubeObjectListLayout = withInjectables<
subscribeToStores: di.inject(subscribeStoresInjectable),
kubeSelectedUrlParam: di.inject(kubeSelectedUrlParamInjectable),
toggleKubeDetailsPane: di.inject(toggleKubeDetailsPaneInjectable),
columns: di.injectMany(kubeObjectListLayoutColumnInjectionToken),
}),
}) as <
K extends KubeObject,

View File

@ -20,7 +20,7 @@ import { getSorted } from "./sorting";
import type { TableModel } from "./table-model/table-model";
import { withInjectables } from "@ogre-tools/injectable-react";
import tableModelInjectable from "./table-model/table-model.injectable";
import type { ItemObject } from "../../../common/item.store";
import type { ItemObject } from "@k8slens/list-layout";
import assert from "assert";
import orderByUrlParamInjectable from "./order-by-url-param.injectable";
import sortByUrlParamInjectable from "./sort-by-url-param.injectable";

View File

@ -5,7 +5,7 @@
import autoBind from "auto-bind";
import type { ItemObject } from "../../common/item.store";
import type { ItemObject } from "@k8slens/list-layout";
export type ForwardedPortStatus = "Active" | "Disabled";
export interface ForwardedPort {

View File

@ -0,0 +1,6 @@
module.exports = {
extends: "@k8slens/eslint-config/eslint",
parserOptions: {
project: "./tsconfig.json",
},
};

View File

@ -0,0 +1 @@
"@k8slens/eslint-config/prettier"

View File

@ -0,0 +1 @@
export * from "./kube-object-list-layout-column-injection-token";

View File

@ -0,0 +1 @@
module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact;

View File

@ -0,0 +1,97 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ReactNode } from "react";
import type { SingleOrMany } from "@k8slens/utilities";
import { getInjectionToken } from "@ogre-tools/injectable";
export interface ItemObject {
getId(): string;
getName(): string;
}
export type TableSortBy = string;
export type TableOrderBy = "asc" | "desc";
export interface TableSortParams {
sortBy: TableSortBy;
orderBy: TableOrderBy;
}
export type TableSortCallback<Item> = (data: Item) => undefined | string | number | (string | number)[];
export type TableSortCallbacks<Item> = Record<string, TableSortCallback<Item>>;
export type SearchFilter<I extends ItemObject> = (item: I) => SingleOrMany<string | number | undefined | null>;
export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
/**
* used for configuration visibility of columns
*/
id?: string;
/**
* Any css class names for this table cell. Only used if `title` is a "simple" react node
*/
className?: string;
/**
* The actual value of the cell
*/
title?: ReactNode;
/**
* content inside could be scrolled
*/
scrollable?: boolean;
/**
* render cell with a checkbox
*/
checkbox?: boolean;
/**
* mark checkbox as checked or not
*/
isChecked?: boolean;
/**
* column name, must be same as key in sortable object <Table sortable={}/>
*/
sortBy?: TableSortBy;
/**
* id of the column which follow same visibility rules
*/
showWithColumn?: string;
/**
* @internal
*/
_sorting?: Partial<TableSortParams>;
/**
* @internal
*/
_sort?(sortBy: TableSortBy): void;
/**
* @internal
* indicator, might come from parent <TableHead>, don't use this prop outside (!)
*/
_nowrap?: boolean;
}
export interface KubeObjectListLayoutColumn<Item extends ItemObject> {
id: string;
kind: string;
apiVersion: string;
priority: number;
sortingCallBack?: TableSortCallback<Item>;
searchFilter?: SearchFilter<Item>;
header: TableCellProps | undefined | null;
content: (item: Item) => ReactNode | TableCellProps;
}
export const kubeObjectListLayoutColumnInjectionToken = getInjectionToken<KubeObjectListLayoutColumn<any>>({
id: "kube-object-list-layout-column",
});

View File

@ -0,0 +1,43 @@
{
"name": "@k8slens/list-layout",
"private": false,
"version": "1.0.0-alpha.0",
"description": "Injection tokens for list layout",
"type": "commonjs",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "git+https://github.com/lensapp/lens.git"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": {
"name": "OpenLens Authors",
"email": "info@k8slens.dev"
},
"license": "MIT",
"homepage": "https://github.com/lensapp/lens",
"scripts": {
"build": "webpack",
"clean": "rimraf dist/",
"dev": "webpack --mode=development --watch",
"test": "jest --coverage --runInBand",
"lint": "lens-lint",
"lint:fix": "lens-lint --fix"
},
"peerDependencies": {
"@ogre-tools/injectable": "^15.1.2",
"react": "^17.0.2"
},
"devDependencies": {
"@k8slens/eslint-config": "^6.5.0-alpha.2",
"@k8slens/jest": "^6.5.0-alpha.2",
"@k8slens/typescript": "^6.5.0-alpha.2"
}
}

View File

@ -0,0 +1,4 @@
{
"extends": "@k8slens/typescript/config/base.json",
"include": ["**/*.ts"]
}

View File

@ -0,0 +1 @@
module.exports = require("@k8slens/webpack").configForReact;