Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UI v2] feat: Adds deployment parameter table #17059

Merged
merged 1 commit into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { type JSX } from "react";

import { DeploymentConfiguration } from "./deployment-configuration";
import { DeploymentDescription } from "./deployment-description";
import { DeploymentParametersTable } from "./deployment-parameters-table";

const routeApi = getRouteApi("/deployments/deployment/$id");

Expand Down Expand Up @@ -59,7 +60,7 @@ export const DeploymentDetailsTabs = ({
),
ViewComponent: () => (
<TabsContent value="Parameters">
<div className="border border-red-400">{"<ParametersView />"}</div>
<DeploymentParametersTable deployment={deployment} />
</TabsContent>
),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Meta, StoryObj } from "@storybook/react";

import { createFakeDeployment } from "@/mocks";
import { DeploymentParametersTable } from "./deployment-parameters-table";

const meta = {
title: "Components/Deployments/DeploymentParametersTable",
component: DeploymentParametersTable,
args: {
deployment: createFakeDeployment({
parameter_openapi_schema: {
title: "Parameters",
type: "object",
properties: {
name: {
default: "world",
position: 0,
title: "name",
type: "string",
},
goodbye: {
default: false,
position: 1,
title: "goodbye",
type: "boolean",
},
},
},
parameters: {
// @ts-expect-error TODO: Fix OpenAPI Schema
goodbye: false,
// @ts-expect-error TODO: Fix OpenAPI Schema
name: "world",
},
}),
},
} satisfies Meta<typeof DeploymentParametersTable>;

export default meta;

export const story: StoryObj = { name: "DeploymentParametersTable" };
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";

import { createFakeDeployment } from "@/mocks";
import { DeploymentParametersTable } from "./deployment-parameters-table";

const MOCK_DEPLOYMENT = createFakeDeployment({
parameter_openapi_schema: {
title: "Parameters",
type: "object",
properties: {
name: {
default: "world",
position: 0,
title: "name",
type: "string",
},
goodbye: {
default: false,
position: 1,
title: "goodbye",
type: "boolean",
},
},
},
parameters: {
// @ts-expect-error TODO: Fix OpenAPI Schema
goodbye: false,
// @ts-expect-error TODO: Fix OpenAPI Schema
name: "world",
},
});

describe("DeploymentParametersTable", () => {
it("renders table with rows", () => {
// ------------ Setup
render(<DeploymentParametersTable deployment={MOCK_DEPLOYMENT} />);

// ------------ Assert
expect(screen.getByRole("cell", { name: /name/i })).toBeVisible();
expect(screen.getByRole("cell", { name: /goodbye/i })).toBeVisible();
});

it("filters table rows", async () => {
// ------------ Setup
const user = userEvent.setup();

render(<DeploymentParametersTable deployment={MOCK_DEPLOYMENT} />);

// ------------ Act
await user.type(screen.getByRole("textbox"), "name");

// ------------ Assert
await waitFor(() =>
expect(
screen.queryByRole("cell", { name: /goodbye/i }),
).not.toBeInTheDocument(),
);
expect(screen.getByRole("cell", { name: /name/i })).toBeVisible();
});

it("filters no results", async () => {
// ------------ Setup
const user = userEvent.setup();

render(<DeploymentParametersTable deployment={MOCK_DEPLOYMENT} />);

// ------------ Act
await user.type(screen.getByRole("textbox"), "no results found");

// ------------ Assert
await waitFor(() =>
expect(
screen.queryByRole("cell", { name: /goodbye/i }),
).not.toBeInTheDocument(),
);
expect(
screen.queryByRole("cell", { name: /name/i }),
).not.toBeInTheDocument();

expect(screen.getByText("No results.")).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Deployment } from "@/api/deployments";
import { DataTable } from "@/components/ui/data-table";
import { SearchInput } from "@/components/ui/input";
import { Typography } from "@/components/ui/typography";
import { pluralize } from "@/utils";
import {
createColumnHelper,
getCoreRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table";
import { useDeferredValue, useMemo, useState } from "react";

type DeploymentParametersTableProps = {
deployment: Deployment;
};

type ParameterOpenApiSchema = {
default: unknown;
position: number;
title: string;
type: "boolean" | "number" | "null" | "string";
};

type ParametersTableColumns = {
key: string;
value: unknown;
defaultValue: unknown;
type: string | undefined;
};

const columnHelper = createColumnHelper<ParametersTableColumns>();

const columns = [
columnHelper.accessor("key", { header: "Key" }),
columnHelper.accessor("value", { header: "Override" }),
columnHelper.accessor("defaultValue", { header: "Default" }),
columnHelper.accessor("type", { header: "Type" }),
];

/**
*
* @param deployment
* @returns converts a deployment schema into a parameters table schema
*/
const useDeploymentParametersToTable = (
deployment: Deployment,
): Array<ParametersTableColumns> =>
useMemo(() => {
const parameterOpenApiSchema = deployment.parameter_openapi_schema
?.properties as Record<string, ParameterOpenApiSchema>;
const parameters = deployment.parameters as Record<string, unknown>;
return Object.keys(parameterOpenApiSchema)
.sort((a, b) => {
return (
parameterOpenApiSchema[a].position -
parameterOpenApiSchema[b].position
);
})
.map((key) => {
const parameter = parameterOpenApiSchema[key];
return {
key,
value: parameters[key],
defaultValue: parameter.default,
type: parameter.type,
};
});
}, [deployment]);

export const DeploymentParametersTable = ({
deployment,
}: DeploymentParametersTableProps) => {
const [search, setSearch] = useState("");
const data = useDeploymentParametersToTable(deployment);

// nb: This table does search via client side
const deferredSearch = useDeferredValue(search);
const filteredData = useMemo(() => {
return data.filter(
(parameter) =>
parameter.key.toLowerCase().includes(deferredSearch.toLowerCase()) ||
parameter.value
?.toString()
.toLowerCase()
.includes(deferredSearch.toLowerCase()) ||
parameter.defaultValue
?.toString()
.toLowerCase()
.includes(deferredSearch.toLowerCase()) ||
parameter.type
?.toString()
.toLowerCase()
.includes(deferredSearch.toLowerCase()),
);
}, [data, deferredSearch]);

const table = useReactTable({
data: filteredData,

columns,
getCoreRowModel: getCoreRowModel(),
defaultColumn: { maxSize: 300 },
getPaginationRowModel: getPaginationRowModel(), //load client-side pagination code
});

return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<Typography variant="bodySmall" className="text-muted-foreground">
{filteredData.length} {pluralize(filteredData.length, "parameter")}
</Typography>
<div className="sm:col-span-2 md:col-span-2 lg:col-span-3">
<SearchInput
className="sm:col-span-2 md:col-span-2 lg:col-span-3"
placeholder="Search parameters"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<DataTable table={table} />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DeploymentParametersTable } from "./deployment-parameters-table";