Skip to content

Commit 1ee80af

Browse files
committed
Improved save layout
1 parent 59d608e commit 1ee80af

2 files changed

Lines changed: 58 additions & 70 deletions

File tree

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.metrics.custom.$dashboardId/route.tsx

Lines changed: 5 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,7 @@ import { redirectWithSuccessMessage } from "~/models/message.server";
4343
import { findProjectBySlug } from "~/models/project.server";
4444
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
4545
import { getAllTaskIdentifiers } from "~/models/task.server";
46-
import {
47-
LayoutItem,
48-
MetricDashboardPresenter,
49-
} from "~/presenters/v3/MetricDashboardPresenter.server";
46+
import { MetricDashboardPresenter } from "~/presenters/v3/MetricDashboardPresenter.server";
5047
import { QueryPresenter } from "~/presenters/v3/QueryPresenter.server";
5148
import { requireUser, requireUserId } from "~/services/session.server";
5249
import { SimpleTooltip } from "~/components/primitives/Tooltip";
@@ -118,29 +115,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
118115
});
119116
};
120117

121-
const SaveLayoutSchema = z.object({
122-
layout: z.string().transform((str, ctx) => {
123-
try {
124-
const parsed = JSON.parse(str);
125-
const result = z.array(LayoutItem).safeParse(parsed);
126-
if (!result.success) {
127-
ctx.addIssue({
128-
code: z.ZodIssueCode.custom,
129-
message: "Invalid layout format",
130-
});
131-
return z.NEVER;
132-
}
133-
return result.data;
134-
} catch {
135-
ctx.addIssue({
136-
code: z.ZodIssueCode.custom,
137-
message: "Invalid JSON",
138-
});
139-
return z.NEVER;
140-
}
141-
}),
142-
});
143-
144118
export const action = async ({ request, params }: ActionFunctionArgs) => {
145119
const userId = await requireUserId(request);
146120
const { projectParam, organizationSlug, envParam, dashboardId } = ParamSchema.parse(params);
@@ -195,34 +169,6 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
195169

196170
return typedjson({ success: true });
197171
}
198-
case "layout": {
199-
const result = SaveLayoutSchema.safeParse({
200-
layout: formData.get("layout"),
201-
});
202-
203-
if (!result.success) {
204-
throw new Response("Invalid form data: " + result.error.message, { status: 400 });
205-
}
206-
207-
// Parse existing layout to preserve widgets
208-
const existingLayout = JSON.parse(dashboard.layout) as Record<string, unknown>;
209-
210-
// Update layout positions while preserving widgets
211-
const updatedLayout = {
212-
...existingLayout,
213-
layout: result.data.layout,
214-
};
215-
216-
// Save to database
217-
await prisma.metricsDashboard.update({
218-
where: { id: dashboard.id },
219-
data: {
220-
layout: JSON.stringify(updatedLayout),
221-
},
222-
});
223-
224-
return typedjson({ success: true });
225-
}
226172
default: {
227173
throw new Response("Invalid action", { status: 400 });
228174
}
@@ -255,9 +201,9 @@ export default function Page() {
255201
const canExceedWidgets =
256202
typeof planLimits === "object" && planLimits.canExceed === true;
257203

258-
// Build the action URLs
204+
// Build the action URLs - both use the resource route to avoid full page renders on POST
259205
const widgetActionUrl = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/dashboards/${friendlyId}/widgets`;
260-
const layoutActionUrl = ""; // Uses form action on current route
206+
const layoutActionUrl = widgetActionUrl;
261207

262208
// Handle sync errors by showing a toast
263209
const handleSyncError = useCallback((error: Error, action: string) => {
@@ -393,11 +339,10 @@ export default function Page() {
393339
<NavBar>
394340
<PageTitle title={title} />
395341
<PageAccessories>
396-
<<<<<<< ours
397342
{widgetIsAtLimit ? (
398343
<Dialog>
399344
<DialogTrigger asChild>
400-
<Button variant="tertiary/small" LeadingIcon={PlusIcon}>
345+
<Button variant="secondary/small" LeadingIcon={PlusIcon}>
401346
Add chart
402347
</Button>
403348
</DialogTrigger>
@@ -422,22 +367,13 @@ export default function Page() {
422367
</Dialog>
423368
) : (
424369
<Button
425-
variant="tertiary/small"
370+
variant="secondary/small"
426371
LeadingIcon={PlusIcon}
427372
onClick={actions.openAddEditor}
428373
>
429374
Add chart
430375
</Button>
431376
)}
432-
||||||| ancestor
433-
<Button variant="tertiary/small" LeadingIcon={PlusIcon} onClick={actions.openAddEditor}>
434-
Add chart
435-
</Button>
436-
=======
437-
<Button variant="secondary/small" LeadingIcon={PlusIcon} onClick={actions.openAddEditor}>
438-
Add chart
439-
</Button>
440-
>>>>>>> theirs
441377
<Popover>
442378
<PopoverVerticalEllipseTrigger variant="secondary" />
443379
<PopoverContent className="w-fit min-w-[10rem] p-1" align="end">

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import { prisma } from "~/db.server";
66
import { QueryWidgetConfig } from "~/components/metrics/QueryWidget";
77
import { findProjectBySlug } from "~/models/project.server";
88
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
9-
import { DashboardLayout } from "~/presenters/v3/MetricDashboardPresenter.server";
9+
import {
10+
DashboardLayout,
11+
LayoutItem,
12+
} from "~/presenters/v3/MetricDashboardPresenter.server";
1013
import { getCurrentPlan } from "~/services/platform.v3.server";
1114
import { requireUserId } from "~/services/session.server";
1215
import { EnvironmentParamSchema, v3CustomDashboardPath } from "~/utils/pathBuilder";
@@ -76,6 +79,29 @@ const DuplicateWidgetSchema = z.object({
7679
widgetId: z.string().min(1, "Widget ID is required"),
7780
});
7881

82+
const SaveLayoutSchema = z.object({
83+
layout: z.string().transform((str, ctx) => {
84+
try {
85+
const parsed = JSON.parse(str);
86+
const result = z.array(LayoutItem).safeParse(parsed);
87+
if (!result.success) {
88+
ctx.addIssue({
89+
code: z.ZodIssueCode.custom,
90+
message: "Invalid layout format",
91+
});
92+
return z.NEVER;
93+
}
94+
return result.data;
95+
} catch {
96+
ctx.addIssue({
97+
code: z.ZodIssueCode.custom,
98+
message: "Invalid JSON",
99+
});
100+
return z.NEVER;
101+
}
102+
}),
103+
});
104+
79105
const ParamsSchema = EnvironmentParamSchema.extend({
80106
dashboardId: z.string(),
81107
});
@@ -428,6 +454,32 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
428454
return typedjson({ success: true, duplicatedTitle: originalWidget.title });
429455
}
430456

457+
case "layout": {
458+
const result = SaveLayoutSchema.safeParse({
459+
layout: formData.get("layout"),
460+
});
461+
462+
if (!result.success) {
463+
throw new Response("Invalid form data: " + result.error.message, { status: 400 });
464+
}
465+
466+
// Update layout positions while preserving widgets
467+
const updatedLayout = {
468+
...existingLayout,
469+
layout: result.data.layout,
470+
};
471+
472+
// Save to database
473+
await prisma.metricsDashboard.update({
474+
where: { id: dashboard.id },
475+
data: {
476+
layout: JSON.stringify(updatedLayout),
477+
},
478+
});
479+
480+
return typedjson({ success: true });
481+
}
482+
431483
default: {
432484
throw new Response("Invalid action", { status: 400 });
433485
}

0 commit comments

Comments
 (0)