diff --git a/src/renderer/components/map/__snapshots__/map.test.tsx.snap b/src/renderer/components/map/__snapshots__/map.test.tsx.snap
new file mode 100644
index 0000000000..e6e69f554a
--- /dev/null
+++ b/src/renderer/components/map/__snapshots__/map.test.tsx.snap
@@ -0,0 +1,58 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Map given items and placeholder but no separator renders 1`] = `
+
+
+
+`;
+
+exports[`Map given more than one item and separator renders 1`] = `
+
+
+
+
+ Some separator
+
+
+
+ Some separator
+
+
+
+
+`;
+
+exports[`Map given no items and placeholder renders 1`] = `
+
+
+
+`;
+
+exports[`Map given no items but no placeholder renders 1`] = `
+
+
+
+`;
diff --git a/src/renderer/components/map/add-separator/add-separator.test.ts b/src/renderer/components/map/add-separator/add-separator.test.ts
new file mode 100644
index 0000000000..8a45b45739
--- /dev/null
+++ b/src/renderer/components/map/add-separator/add-separator.test.ts
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { addSeparator } from "./add-separator";
+
+describe("add-separator", () => {
+ it("given multiple items, adds separators", () => {
+ const items = ["first", "second", "third"];
+
+ const actual = addSeparator((left, right) => `separator-between-${left}-and-${right}`, items);
+
+ expect(actual).toEqual([
+ "first",
+ "separator-between-first-and-second",
+ "second",
+ "separator-between-second-and-third",
+ "third",
+ ]);
+ });
+
+ it("given multiple items including falsy ones, adds separators", () => {
+ const items = [false, undefined, null, NaN];
+
+ const actual = addSeparator((left, right) => `separator-between-${left}-and-${right}`, items);
+
+ expect(actual).toEqual([
+ false,
+ "separator-between-false-and-undefined",
+ undefined,
+ "separator-between-undefined-and-null",
+ null,
+ "separator-between-null-and-NaN",
+ NaN,
+ ]);
+ });
+
+ it("given no items, does not add separator", () => {
+ const items: any[] = [];
+
+ const actual = addSeparator(() => "separator", items);
+
+ expect(actual).toEqual([]);
+ });
+
+ it("given one item, does not add separator", () => {
+ const items = ["first"];
+
+ const actual = addSeparator(() => "separator", items);
+
+ expect(actual).toEqual(["first"]);
+ });
+});
diff --git a/src/renderer/components/map/add-separator/add-separator.ts b/src/renderer/components/map/add-separator/add-separator.ts
new file mode 100644
index 0000000000..d0d87c9a3f
--- /dev/null
+++ b/src/renderer/components/map/add-separator/add-separator.ts
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+
+type GetSeparator- = (left: Item, right: Item) => Separator;
+
+export const addSeparator =
- (
+ getSeparator: GetSeparator
- ,
+ items: Item[],
+) => items.flatMap(toSeparatedTupleUsing(getSeparator));
+
+const toSeparatedTupleUsing =
+
- (getSeparator: GetSeparator
- ) =>
+ (leftItem: Item, index: number, arr: Item[]) => {
+ const itemIsLast = arr.length === index + 1;
+
+ if (itemIsLast) {
+ return [leftItem];
+ }
+
+ const rightItem = arr[index + 1];
+ const separator = getSeparator(leftItem, rightItem);
+
+ return [leftItem, separator];
+ };
diff --git a/src/renderer/components/map/map.test.tsx b/src/renderer/components/map/map.test.tsx
new file mode 100644
index 0000000000..4171f1a1c1
--- /dev/null
+++ b/src/renderer/components/map/map.test.tsx
@@ -0,0 +1,121 @@
+/**
+ * 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 { render } from "@testing-library/react";
+import React from "react";
+import { Map } from "./map";
+
+describe("Map", () => {
+ describe("given no items and placeholder", () => {
+ let rendered: RenderResult;
+
+ beforeEach(() => {
+ rendered = render(
+ ,
+ );
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("renders placeholder", () => {
+ expect(rendered.getByTestId("some-placeholder")).toBeInTheDocument();
+ });
+
+ it("does not render rows", () => {
+ expect(rendered.queryByTestId("some-row")).not.toBeInTheDocument();
+ });
+ });
+
+ describe("given no items but no placeholder", () => {
+ let rendered: RenderResult;
+
+ beforeEach(() => {
+ rendered = render(
+ ,
+ );
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not render rows", () => {
+ expect(rendered.queryByTestId("some-row")).not.toBeInTheDocument();
+ });
+ });
+
+ describe("given items and placeholder but no separator", () => {
+ let rendered: RenderResult;
+
+ beforeEach(() => {
+ rendered = render(
+ ,
+ );
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not render placeholder", () => {
+ expect(
+ rendered.queryByTestId("some-placeholder"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("renders items", () => {
+ expect(rendered.getByTestId("some-item-id")).toBeInTheDocument();
+ });
+ });
+
+ describe("given more than one item and separator", () => {
+ let rendered: RenderResult;
+
+ beforeEach(() => {
+ rendered = render(
+ ,
+ );
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("renders items", () => {
+ expect(rendered.getByTestId("some-item-id")).toBeInTheDocument();
+ });
+
+ it("renders separator", () => {
+ expect(rendered.getAllByTestId("separator")).toHaveLength(2);
+ });
+ });
+});
diff --git a/src/renderer/components/map/map.tsx b/src/renderer/components/map/map.tsx
new file mode 100644
index 0000000000..ad33963002
--- /dev/null
+++ b/src/renderer/components/map/map.tsx
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { pipeline } from "@ogre-tools/fp";
+import { identity, map } from "lodash/fp";
+import React from "react";
+import { addSeparator } from "./add-separator/add-separator";
+
+interface RequiredPropertiesForItem {
+ id: string;
+}
+
+interface MapProps
- {
+ items: Item[];
+ children: (item: Item) => React.ReactElement;
+ getPlaceholder?: () => React.ReactElement;
+ getSeparator?: () => React.ReactElement;
+}
+
+export const Map =
- (
+ props: MapProps
- ,
+) => {
+ const { items, getPlaceholder, getSeparator, children } = props;
+
+ if (items.length === 0) {
+ return getPlaceholder?.() || null;
+ }
+
+ return (
+ <>
+ {pipeline(
+ items,
+
+ map((item) => ({ item, render: () => children(item) })),
+
+ getSeparator
+ ? (items) =>
+ addSeparator(
+ (left, right) => ({
+ item: {
+ id: `separator-between-${left.item.id}-and-${right.item.id}`,
+ },
+
+ render: getSeparator,
+ }),
+
+ items,
+ )
+ : identity,
+
+ map(({ render, item }) => (
+ {render()}
+ )),
+ )}
+ >
+ );
+};