From 51462a130ef390b45a58b7e05f5b2192010cb853 Mon Sep 17 00:00:00 2001 From: sirily11 <32106111+sirily11@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:58:13 +0800 Subject: [PATCH] fix: chart offset and width issue --- examples/server/main.go | 64 ++++++ examples/web/e2e/crud.spec.ts | 8 +- .../src/components/custom-resource-page.tsx | 194 ++++++++++-------- .../src/components/resource-view.test.tsx | 47 ++++- 4 files changed, 217 insertions(+), 96 deletions(-) diff --git a/examples/server/main.go b/examples/server/main.go index fd26f2b..410989a 100644 --- a/examples/server/main.go +++ b/examples/server/main.go @@ -96,6 +96,70 @@ func main() { }, }, }, + { + Type: admin.CustomPageSectionCharts, + Title: "Audience", + Children: []admin.Chart{ + { + Type: admin.ChartTypeLine, + Title: "Subscribers", + Data: []map[string]any{ + {"day": "Mon", "subscribers": 42}, + {"day": "Tue", "subscribers": 58}, + {"day": "Wed", "subscribers": 73}, + {"day": "Thu", "subscribers": 81}, + {"day": "Fri", "subscribers": 96}, + }, + XKey: "day", + YKey: "subscribers", + }, + }, + }, + { + Type: admin.CustomPageSectionCharts, + Title: "Publishing pipeline", + Children: []admin.Chart{ + { + Type: admin.ChartTypeLine, + Title: "Drafts created", + Data: []map[string]any{ + {"day": "Mon", "drafts": 4}, + {"day": "Tue", "drafts": 7}, + {"day": "Wed", "drafts": 5}, + {"day": "Thu", "drafts": 9}, + {"day": "Fri", "drafts": 6}, + }, + XKey: "day", + YKey: "drafts", + }, + { + Type: admin.ChartTypeLine, + Title: "Reviews completed", + Data: []map[string]any{ + {"day": "Mon", "reviews": 3}, + {"day": "Tue", "reviews": 4}, + {"day": "Wed", "reviews": 6}, + {"day": "Thu", "reviews": 5}, + {"day": "Fri", "reviews": 8}, + }, + XKey: "day", + YKey: "reviews", + }, + { + Type: admin.ChartTypeLine, + Title: "Posts scheduled", + Data: []map[string]any{ + {"day": "Mon", "scheduled": 2}, + {"day": "Tue", "scheduled": 3}, + {"day": "Wed", "scheduled": 4}, + {"day": "Thu", "scheduled": 7}, + {"day": "Fri", "scheduled": 5}, + }, + XKey: "day", + YKey: "scheduled", + }, + }, + }, { Type: admin.CustomPageSectionText, Title: "Editorial note", diff --git a/examples/web/e2e/crud.spec.ts b/examples/web/e2e/crud.spec.ts index a94e40f..861fccc 100644 --- a/examples/web/e2e/crud.spec.ts +++ b/examples/web/e2e/crud.spec.ts @@ -50,8 +50,13 @@ test("custom dashboard page renders from the backend schema", async ({ page }) = await expect(main.getByText("Published posts")).toBeVisible(); await expect(main.getByText("Monthly views")).toBeVisible(); await expect(main.getByText("Views by day")).toBeVisible(); - await expect(main.getByText("views", { exact: true })).toBeVisible(); await expect(main.getByText("Engagement")).toBeVisible(); + await expect(main.getByText("Audience")).toBeVisible(); + const audienceSection = main.locator("section").filter({ hasText: "Audience" }); + await expect(audienceSection.getByText(/^Subscribers$/)).toBeVisible(); + await expect( + audienceSection.locator('[data-chart-type="line"]'), + ).toHaveCount(1); await expect(main.getByText("Editorial note")).toBeVisible(); await expect(main.getByText("Review draft posts weekly")).toBeVisible(); @@ -66,6 +71,7 @@ test("custom dashboard page renders from the backend schema", async ({ page }) = await main .locator('[data-chart-type="line"] svg[role="application"]') + .first() .hover({ position: { x: 80, y: 120 } }); await expect(tooltip).toContainText("Tue"); await expect(tooltip).toContainText("Reads"); diff --git a/packages/admin-next/src/components/custom-resource-page.tsx b/packages/admin-next/src/components/custom-resource-page.tsx index 5876e3f..391f3b4 100644 --- a/packages/admin-next/src/components/custom-resource-page.tsx +++ b/packages/admin-next/src/components/custom-resource-page.tsx @@ -9,6 +9,7 @@ import { BarChart, CartesianGrid, Legend, + ResponsiveContainer, Tooltip, XAxis, YAxis, @@ -113,21 +114,34 @@ function ChartSection({ }: { section: Extract; }): React.JSX.Element { + const charts = section.children ?? []; + const gridClassName = + charts.length === 1 ? "grid gap-3" : "grid gap-3 @2xl/main:grid-cols-2"; + return (
-
- {(section.children ?? []).map((chart, index) => ( -
- {chart.title && ( -
{chart.title}
- )} - -
- ))} +
+ {charts.map((chart, index) => { + const isOddLastChart = + charts.length > 1 && + charts.length % 2 === 1 && + index === charts.length - 1; + + return ( +
+ {chart.title && ( +
{chart.title}
+ )} + +
+ ); + })}
); @@ -201,88 +215,88 @@ function SimpleChart({ chart }: { chart: Chart }): React.JSX.Element { } return ( -
- {kind === "line" ? ( - - - - - } - cursor={{ stroke: "var(--border)" }} - offset={0} - /> - - {series.map((s, index) => { - const color = s.color ?? paletteColor(index); - return ( - + + {kind === "line" ? ( + + + + + } + cursor={{ stroke: "var(--border)" }} + offset={0} + /> + + {series.map((s, index) => { + const color = s.color ?? paletteColor(index); + return ( + + ); + })} + + ) : ( + + + + + } + cursor={{ fill: "var(--muted)" }} + offset={0} + /> + + {series.map((s, index) => ( + - ); - })} - - ) : ( - - - - - } - cursor={{ fill: "var(--muted)" }} - offset={0} - /> - - {series.map((s, index) => ( - - ))} - - )} + ))} + + )} +
); } diff --git a/packages/admin-next/src/components/resource-view.test.tsx b/packages/admin-next/src/components/resource-view.test.tsx index 6add10d..a8c933a 100644 --- a/packages/admin-next/src/components/resource-view.test.tsx +++ b/packages/admin-next/src/components/resource-view.test.tsx @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import type { CustomResourcePage, ResourceInfo, TableSchema } from "../types.js"; import type { AdminActions } from "../server/actions.js"; @@ -24,10 +24,45 @@ const noopActions: AdminActions = { submitAction: vi.fn(), }; +class TestResizeObserver implements ResizeObserver { + private callback: ResizeObserverCallback; + + constructor(callback: ResizeObserverCallback) { + this.callback = callback; + } + + observe(target: Element): void { + this.callback( + [ + { + target, + contentRect: DOMRectReadOnly.fromRect({ + width: 640, + height: 256, + }), + } as ResizeObserverEntry, + ], + this, + ); + } + + unobserve(): void {} + + disconnect(): void {} +} + +beforeEach(() => { + vi.stubGlobal("ResizeObserver", TestResizeObserver); + vi.spyOn(Element.prototype, "getBoundingClientRect").mockReturnValue( + DOMRect.fromRect({ width: 640, height: 256 }), + ); +}); + afterEach(() => { routerPush.mockReset(); window.history.replaceState(null, "", "/"); vi.restoreAllMocks(); + vi.unstubAllGlobals(); }); describe("ResourceView", () => { @@ -233,7 +268,7 @@ describe("ResourceView", () => { expect(routerPush).not.toHaveBeenCalled(); }); - it("renders a custom resource page with actions and sections", () => { + it("renders a custom resource page with actions and sections", async () => { const customSchema: CustomResourcePage = { uiType: "custom", type: "view", @@ -310,9 +345,11 @@ describe("ResourceView", () => { expect(screen.getByText("Posts")).toBeTruthy(); expect(screen.getByText("25")).toBeTruthy(); expect(screen.getByText("Views")).toBeTruthy(); - expect(screen.getByText("views")).toBeTruthy(); - expect(screen.getByText("Reads")).toBeTruthy(); - expect(screen.getByText("Shares")).toBeTruthy(); + await waitFor(() => { + expect(screen.getByText("views")).toBeTruthy(); + expect(screen.getByText("Reads")).toBeTruthy(); + expect(screen.getByText("Shares")).toBeTruthy(); + }); expect(screen.getByText("Review pending posts before publishing.")).toBeTruthy(); }); });