diff --git a/packages/core/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts b/packages/core/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts
index 38eb14aad8..bc579b1c8e 100644
--- a/packages/core/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts
+++ b/packages/core/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts
@@ -9,7 +9,7 @@ import type { Discriminable } from "../../../../common/utils/composable-responsi
import type { Labelable } from "../../../../common/utils/composable-responsibilities/labelable/labelable";
import type { MaybeShowable } from "../../../../common/utils/composable-responsibilities/showable/showable";
import type { Orderable } from "../../../../common/utils/composable-responsibilities/orderable/orderable";
-import type { GetSeparator } from "../../../../common/utils/add-separator/add-separator";
+import type { GetSeparator } from "@k8slens/utilities";
import type { Composite } from "../../../../common/utils/composite/get-composite/get-composite";
export type ChildrenAreSeparated =
diff --git a/packages/core/src/renderer/components/map/map.tsx b/packages/core/src/renderer/components/map/map.tsx
index f3b660d041..82a55bd0bc 100644
--- a/packages/core/src/renderer/components/map/map.tsx
+++ b/packages/core/src/renderer/components/map/map.tsx
@@ -5,8 +5,8 @@
import { pipeline } from "@ogre-tools/fp";
import { identity, map } from "lodash/fp";
import React from "react";
-import type { GetSeparator } from "../../../common/utils/add-separator/add-separator";
-import { addSeparator } from "../../../common/utils/add-separator/add-separator";
+import type { GetSeparator } from "@k8slens/utilities";
+import { addSeparator } from "@k8slens/utilities";
interface RequiredPropertiesForItem {
id: string;
diff --git a/packages/technical-features/ui-components/index.ts b/packages/technical-features/ui-components/index.ts
index b60fb0485a..a70b921258 100644
--- a/packages/technical-features/ui-components/index.ts
+++ b/packages/technical-features/ui-components/index.ts
@@ -1,3 +1,5 @@
export { uiComponentsFeature } from "./src/feature";
export * from "./src/element/elements";
+export * from "./src/map/map";
+export * from "./src/gutter/gutter";
diff --git a/packages/technical-features/ui-components/package.json b/packages/technical-features/ui-components/package.json
index 079163c475..4e6be480d9 100644
--- a/packages/technical-features/ui-components/package.json
+++ b/packages/technical-features/ui-components/package.json
@@ -35,15 +35,17 @@
},
"peerDependencies": {
"@k8slens/feature-core": "^6.5.0-alpha.0",
+ "@k8slens/utilities": "^1.0.0-alpha.1",
+ "@ogre-tools/fp": "^15.1.2",
"@ogre-tools/injectable": "^15.1.2",
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2",
- "@ogre-tools/fp": "^15.1.2",
"lodash": "^4.17.21",
"react": "^17"
},
"devDependencies": {
"@async-fn/jest": "^1.6.4",
"@k8slens/eslint-config": "6.5.0-alpha.1",
- "@k8slens/react-testing-library-discovery": "^1.0.0-alpha.0"
+ "@k8slens/react-testing-library-discovery": "^1.0.0-alpha.0",
+ "@testing-library/react": "^12.1.5"
}
}
diff --git a/packages/technical-features/ui-components/src/gutter/__snapshots__/gutter.test.tsx.snap b/packages/technical-features/ui-components/src/gutter/__snapshots__/gutter.test.tsx.snap
new file mode 100644
index 0000000000..2458cd90a3
--- /dev/null
+++ b/packages/technical-features/ui-components/src/gutter/__snapshots__/gutter.test.tsx.snap
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Gutter "when size is md", renders 1`] = `
+
+
+
+`;
+
+exports[`Gutter "when size is not set", renders 1`] = `
+
+
+
+`;
+
+exports[`Gutter "when size is sm", renders 1`] = `
+
+
+
+`;
+
+exports[`Gutter "when size is xl", renders 1`] = `
+
+
+
+`;
diff --git a/packages/technical-features/ui-components/src/gutter/gutter.test.tsx b/packages/technical-features/ui-components/src/gutter/gutter.test.tsx
new file mode 100644
index 0000000000..7526d66004
--- /dev/null
+++ b/packages/technical-features/ui-components/src/gutter/gutter.test.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+
+import { Gutter, GutterProps } from "./Gutter";
+import { render } from "@testing-library/react";
+
+interface Scenario {
+ name: string;
+ props: GutterProps;
+}
+
+describe("Gutter", () => {
+ (
+ [
+ { name: "when size is not set", props: {} },
+ { name: "when size is sm", props: { size: "sm" } },
+ { name: "when size is md", props: { size: "md" } },
+ { name: "when size is xl", props: { size: "xl" } },
+ ] as Scenario[]
+ ).forEach((scenario) => {
+ it(`"${scenario.name}", renders`, () => {
+ const rendered = render();
+
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+ });
+});
diff --git a/packages/technical-features/ui-components/src/gutter/gutter.tsx b/packages/technical-features/ui-components/src/gutter/gutter.tsx
new file mode 100644
index 0000000000..51f817953b
--- /dev/null
+++ b/packages/technical-features/ui-components/src/gutter/gutter.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import type { ShirtSize } from "../shirt-size";
+
+export interface GutterProps {
+ size?: ShirtSize;
+}
+
+const classNamesByShirtSize: Record = {
+ sm: "w-4 h-4",
+ md: "w-6 h-6",
+ xl: "w-10 h-10",
+};
+
+export const Gutter = ({ size = "md" }: GutterProps) => (
+
+);
diff --git a/packages/technical-features/ui-components/src/map/__snapshots__/map.test.tsx.snap b/packages/technical-features/ui-components/src/map/__snapshots__/map.test.tsx.snap
new file mode 100644
index 0000000000..0cd7134778
--- /dev/null
+++ b/packages/technical-features/ui-components/src/map/__snapshots__/map.test.tsx.snap
@@ -0,0 +1,90 @@
+// 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 more than one item and separator using left and right renders 1`] = `
+
+
+
+
+ Some separator between
+ some-item-id
+ and
+ some-other-item-id
+
+
+
+ Some separator between
+ some-other-item-id
+ and
+ some-another-item-id
+
+
+
+
+`;
+
+exports[`Map given no items and placeholder renders 1`] = `
+
+
+
+`;
+
+exports[`Map given no items but no placeholder renders 1`] = `
+
+
+
+`;
diff --git a/packages/technical-features/ui-components/src/map/map.test.tsx b/packages/technical-features/ui-components/src/map/map.test.tsx
new file mode 100644
index 0000000000..976b2e672b
--- /dev/null
+++ b/packages/technical-features/ui-components/src/map/map.test.tsx
@@ -0,0 +1,147 @@
+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);
+ });
+ });
+
+ describe("given more than one item and separator using left and right", () => {
+ 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.getByTestId("separator-between-some-item-id-and-some-other-item-id"),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/technical-features/ui-components/src/map/map.tsx b/packages/technical-features/ui-components/src/map/map.tsx
new file mode 100644
index 0000000000..765a70c7f0
--- /dev/null
+++ b/packages/technical-features/ui-components/src/map/map.tsx
@@ -0,0 +1,50 @@
+import { pipeline } from "@ogre-tools/fp";
+import { identity, map } from "lodash/fp";
+import React from "react";
+import { addSeparator, GetSeparator } from "@k8slens/utilities";
+
+interface RequiredPropertiesForItem {
+ id: string;
+}
+
+export interface MapProps- {
+ items: Item[];
+ children: (item: Item) => React.ReactElement;
+ getPlaceholder?: () => React.ReactElement;
+ getSeparator?: GetSeparator
- ;
+}
+
+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(left.item, right.item),
+ }),
+
+ items,
+ )
+ : identity,
+
+ map(({ render, item }) => {render()}),
+ )}
+ >
+ );
+};
diff --git a/packages/technical-features/ui-components/src/shirt-size.ts b/packages/technical-features/ui-components/src/shirt-size.ts
new file mode 100644
index 0000000000..4b5b150eaa
--- /dev/null
+++ b/packages/technical-features/ui-components/src/shirt-size.ts
@@ -0,0 +1 @@
+export type ShirtSize = "sm" | "md" | "xl";
diff --git a/packages/utility-features/utilities/index.ts b/packages/utility-features/utilities/index.ts
index dc8eacdfdd..3e1797fb16 100644
--- a/packages/utility-features/utilities/index.ts
+++ b/packages/utility-features/utilities/index.ts
@@ -43,3 +43,4 @@ export * from "./src/types";
export * from "./src/union-env-path";
export * from "./src/wait";
export * from "./src/with-concurrency-limit";
+export * from "./src/add-separator/add-separator";
diff --git a/packages/core/src/common/utils/add-separator/add-separator.test.ts b/packages/utility-features/utilities/src/add-separator/add-separator.test.ts
similarity index 100%
rename from packages/core/src/common/utils/add-separator/add-separator.test.ts
rename to packages/utility-features/utilities/src/add-separator/add-separator.test.ts
diff --git a/packages/core/src/common/utils/add-separator/add-separator.ts b/packages/utility-features/utilities/src/add-separator/add-separator.ts
similarity index 100%
rename from packages/core/src/common/utils/add-separator/add-separator.ts
rename to packages/utility-features/utilities/src/add-separator/add-separator.ts