Skip to content

Commit

Permalink
feat: improve config details sections by adding subsections
Browse files Browse the repository at this point in the history
Closes #1386
  • Loading branch information
mainawycliffe committed Oct 28, 2023
1 parent 6b15e11 commit b7977fd
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 47 deletions.
8 changes: 7 additions & 1 deletion src/api/services/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export interface ConfigItem {
id: string;
name: string;
};
config_scrapers?: {
id: string;
name: string;
};
}

export type ConfigTypeRelationships = {
Expand Down Expand Up @@ -143,7 +147,9 @@ export const getAllChanges = (
};

export const getConfig = (id: string) =>
resolve<ConfigItem[]>(ConfigDB.get(`/config_items?id=eq.${id}`));
resolve<ConfigItem[]>(
ConfigDB.get(`/config_items?id=eq.${id}&select=*,config_scrapers(id,name)`)
);

export type ConfigsTagList = {
key: string;
Expand Down
177 changes: 131 additions & 46 deletions src/components/Configs/Sidebar/ConfigDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { useMemo } from "react";
import { FaTags } from "react-icons/fa";
import { Link } from "react-router-dom";
import { useGetConfigByIdQuery } from "../../../api/query-hooks";
import { relativeDateTime } from "../../../utils/date";
import { CountBadge } from "../../Badge/CountBadge";
import CollapsiblePanel from "../../CollapsiblePanel";
import { DescriptionCard } from "../../DescriptionCard";
import { InfoMessage } from "../../InfoMessage";
import TextSkeletonLoader from "../../SkeletonLoader/TextSkeletonLoader";
import Title from "../../Title/title";
import { capitalize } from "lodash";
import DisplayDetailsRow from "../../Utils/DisplayDetailsRow";

type Props = {
configId: string;
Expand All @@ -29,44 +28,35 @@ export function ConfigDetails({

const displayDetails = useMemo(() => {
if (configDetails) {
return [
{
label: "Name",
value: configDetails.name
},
...(configDetails.tags
? Object.entries(configDetails.tags)
.filter(([key]) => key !== "Name")
.map(([key, value]) => ({
label: capitalize(key),
value
}))
: []),
...(configDetails.created_at
? [
{
label: "Created At",
value: relativeDateTime(configDetails.created_at)
}
]
: []),
...(configDetails.updated_at
? [
{
label: "Updated At",
value: relativeDateTime(configDetails.updated_at)
}
]
: []),
...(configDetails.deleted_at
? [
if (!configDetails.tags) {
return undefined;
}

const groupedTags = new Map<string, Record<string, any>[]>();

Object.entries(configDetails.tags)
.filter(([key]) => key.toLowerCase() !== "name")
.forEach(([key, value]) => {
// if we can't split key by slash, then we don't need to group the tags
if (key.split("/").length === 1) {
groupedTags.set(key, [
{
label: "Deleted At",
value: relativeDateTime(configDetails.deleted_at)
key: value
}
]
: [])
];
]);
return;
}
const groupKey = key.split("/")[0];
const existingValues = groupedTags.get(groupKey) ?? [];
groupedTags.set(groupKey, [
...existingValues,
{
[key.split("/").slice(1).join("/")]: value
}
]);
});

return groupedTags;
}
}, [configDetails]);

Expand All @@ -77,19 +67,114 @@ export function ConfigDetails({
Header={
<div className="flex py-2 flex-row flex-1 items-center space-x-2">
<Title title="Tags" icon={<FaTags className="w-6 h-auto" />} />
<CountBadge
roundedClass="rounded-full"
value={displayDetails?.length ?? 0}
/>
</div>
}
dataCount={displayDetails?.length}
>
<div className="flex flex-col space-y-2 py-2 max-w-full">
{isLoading ? (
<TextSkeletonLoader />
) : displayDetails && !error ? (
<DescriptionCard items={displayDetails} labelStyle="top" />
) : configDetails && !error ? (
<>
<DisplayDetailsRow
items={[
{
label: "Name",
value: configDetails.name
}
]}
/>
{configDetails.type && (
<DisplayDetailsRow
items={[
{
label: "Type",
value: configDetails.type
}
]}
/>
)}
<DisplayDetailsRow
items={[
{
label: "Created At",
value: relativeDateTime(configDetails.created_at)
},
{
label: "Updated At",
value: relativeDateTime(configDetails.updated_at)
}
]}
/>

{configDetails.config_scrapers && (
<DisplayDetailsRow
items={[
{
label: "Scrapper",
value: (
<Link
to={`/settings/config_scrapers/${configDetails.config_scrapers.id}`}
>
{configDetails.config_scrapers.name}
</Link>
)
}
]}
/>
)}

{displayDetails &&
Object.entries(Object.fromEntries(displayDetails.entries())).map(
([key, values]) => {
if (values.length === 1) {
return (
<DisplayDetailsRow
key={key}
items={[
{
label: key,
value: Object.values(values[0])[0]
}
]}
/>
);
}
return (
<div key={key} className="flex flex-col gap-2">
<label className="text-sm font-medium capitalize">
{key}
</label>
<div className="flex flex-col gap-2 px-2">
{values.map((k) =>
Object.entries(k).map(([key, value]) => (
<DisplayDetailsRow
key={key}
items={[
{
label: key,
value: value
}
]}
/>
))
)}
</div>
</div>
);
}
)}

{configDetails.deleted_at && (
<DisplayDetailsRow
items={[
{
label: "Deleted At",
value: relativeDateTime(configDetails.deleted_at)
}
]}
/>
)}
</>
) : (
<InfoMessage message="Details not found" />
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { ConfigDetails } from "./../ConfigDetails";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter } from "react-router-dom";

const server = setupServer(
rest.get("/api/db/config_items", (req, res, ctx) => {
return res(
ctx.json([
{
name: "Test Config",
type: "Test Type",
created_at: "2022-01-01T00:00:00.000Z",
updated_at: "2022-01-02T00:00:00.000Z",
tags: {
"Tag 1": "Value 1",
"Tag 2/Subtag 1": "Value 2",
"Tag 2/Subtag 2": "Value 3"
}
}
])
);
})
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

const queryClient = new QueryClient({});

describe("ConfigDetails", () => {
const configId = "123";

it("renders the config name", async () => {
render(
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<ConfigDetails configId={configId} isCollapsed={false} />
</QueryClientProvider>
</MemoryRouter>
);
expect(await screen.findByText("Test Config")).toBeInTheDocument();
});

it("renders the tags", async () => {
render(
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<ConfigDetails configId={configId} isCollapsed={false} />
</QueryClientProvider>
</MemoryRouter>
);
expect(await screen.findByText("Tag 1")).toBeInTheDocument();
expect(await screen.findByText("Subtag 1")).toBeInTheDocument();
expect(await screen.findByText("Subtag 2")).toBeInTheDocument();
});
});
27 changes: 27 additions & 0 deletions src/components/Utils/DisplayDetailsRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ReactNode } from "react";

type DisplayDetailsRowProps = {
items: {
label: string;
value: ReactNode;
}[];
className?: string;
};

export default function DisplayDetailsRow({
items,
className = "flex flex-col flex-1"
}: DisplayDetailsRowProps) {
return (
<div className="flex flex-row gap-2 w-full">
{items.map(({ label, value }) => (
<div className={className} key={label} data-testid="display-item-row">
<label className="text-sm overflow-hidden truncate text-gray-900 font-medium">
{label}
</label>
<p className="text-sm text-gray-500 break-all">{value}</p>
</div>
))}
</div>
);
}
33 changes: 33 additions & 0 deletions src/components/Utils/__tests__/DisplayDetailsRow.unit.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import DisplayDetailsRow from "./../DisplayDetailsRow";

describe("DisplayDetailsRow", () => {
const items = [
{ label: "Label 1", value: "Value 1" },
{ label: "Label 2", value: "Value 2" },
{ label: "Label 3", value: "Value 3" }
];

it("renders the correct number of items", () => {
render(<DisplayDetailsRow items={items} />);
expect(screen.getAllByTestId("display-item-row")).toHaveLength(
items.length
);
});

it("renders the correct labels and values", () => {
render(<DisplayDetailsRow items={items} />);

items.forEach(({ label, value }) => {
expect(screen.getByText(label)).toBeInTheDocument();
expect(screen.getByText(value)).toBeInTheDocument();
});
});

it("renders with the correct className", () => {
const className = "test-class-name";
render(<DisplayDetailsRow items={items} className={className} />);
expect(screen.getAllByTestId("display-item-row")[0]).toHaveClass(className);
});
});

0 comments on commit b7977fd

Please sign in to comment.