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

introduce kube-object-list-layout-column-injection-token

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2023-03-31 08:30:27 +03:00 committed by Sami Tiilikainen
parent 192d8119db
commit afff3f5c71
18 changed files with 3432 additions and 184 deletions

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

@ -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 "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
import { kubeObjectListLayoutColumnInjectionToken } from "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
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 "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
import { kubeObjectListLayoutColumnInjectionToken } from "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
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 "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
import { kubeObjectListLayoutColumnInjectionToken } from "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
import { Tooltip } from "../../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 "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
import { kubeObjectListLayoutColumnInjectionToken } from "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
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 "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
import { kubeObjectListLayoutColumnInjectionToken } from "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
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 "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
import { kubeObjectListLayoutColumnInjectionToken } from "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
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 "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
import { kubeObjectListLayoutColumnInjectionToken } from "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
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 "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
import { kubeObjectListLayoutColumnInjectionToken } from "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
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 "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
import { kubeObjectListLayoutColumnInjectionToken } from "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
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 "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
import { kubeObjectListLayoutColumnInjectionToken } from "../../kube-object-list-layout/kube-object-list-layout-column-injection-token";
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

@ -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

@ -0,0 +1,24 @@
/**
* 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 { SearchFilter } from "../item-object-list/list-layout";
import type { TableCellProps, TableSortCallback } from "../table";
import type { ItemObject } from "../../../common/item.store";
import { getInjectionToken } from "@ogre-tools/injectable";
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

@ -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,10 @@ 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 } from "./kube-object-list-layout-column-injection-token";
import type { ItemObject } from "../../../common/item.store";
import { kubeObjectListLayoutColumnInjectionToken } from "./kube-object-list-layout-column-injection-token";
import { sortBy } from "lodash";
export interface KubeObjectListLayoutProps<
K extends KubeObject,
@ -53,6 +57,7 @@ interface Dependencies {
subscribeToStores: SubscribeStores;
kubeSelectedUrlParam: PageParam<string>;
toggleKubeDetailsPane: ToggleKubeDetailsPane;
columns: KubeObjectListLayoutColumn<ItemObject>[];
}
const getLoadErrorMessage = (error: unknown): string => {
@ -140,9 +145,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 +196,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 +221,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

@ -10,6 +10,7 @@ import setStatusBarStatusInjectable from "./components/status-bar/set-status-bar
// @experimental
export type { Environments } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
export { nodeEnvInjectionToken } from "../common/vars/node-env-injection-token";
export * from "./components/kube-object-list-layout/kube-object-list-layout-column-injection-token";
export { registerLensCore } from "./register-lens-core";
export {
React,