Skip to content

Commit 69d442b

Browse files
committed
feat(webapp): dashboard for dev branches
Add the dev-branches dashboard route and make the branch UI env-aware: - New env.$envParam.dev-branches route for creating/listing/archiving development branches, mirroring the preview branches page. - NewBranchPanel and the branches page take an env ("preview" | "development") instead of a parent environment id. - Environment selector, labels, blank-state panels and environment sort surface dev branches. - Presence resource route is keyed by env param (renamed from dev.presence to env.$envParam.presence); branch archive redirects to the correct (preview vs dev) branches path. TRI-8726
1 parent c0ef8e8 commit 69d442b

10 files changed

Lines changed: 1133 additions & 70 deletions

File tree

apps/webapp/app/components/BlankStatePanels.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -488,24 +488,26 @@ export function BranchesNoBranchableEnvironment({ showSelfServe }: { showSelfSer
488488
}
489489

490490
export function BranchesNoBranches({
491-
parentEnvironment,
491+
envType,
492492
limits,
493493
canUpgrade,
494494
showSelfServe,
495495
}: {
496-
parentEnvironment: { id: string };
496+
envType: "preview" | "development";
497497
limits: { used: number; limit: number };
498498
canUpgrade: boolean;
499499
showSelfServe: boolean;
500500
}) {
501501
const organization = useOrganization();
502502

503+
const envTextClassName = envType === "preview" ? "text-preview" : "text-dev";
504+
503505
if (limits.used >= limits.limit) {
504506
return (
505507
<InfoPanel
506508
title="Upgrade to get preview branches"
507509
icon={BranchEnvironmentIconSmall}
508-
iconClassName="text-preview"
510+
iconClassName={envTextClassName}
509511
panelClassName="max-w-full"
510512
accessory={
511513
showSelfServe && canUpgrade ? (
@@ -536,7 +538,7 @@ export function BranchesNoBranches({
536538
<InfoPanel
537539
title="Create your first branch"
538540
icon={BranchEnvironmentIconSmall}
539-
iconClassName="text-preview"
541+
iconClassName={envTextClassName}
540542
panelClassName="max-w-full"
541543
accessory={
542544
<NewBranchPanel
@@ -549,7 +551,7 @@ export function BranchesNoBranches({
549551
New branch
550552
</Button>
551553
}
552-
parentEnvironment={parentEnvironment}
554+
env="preview"
553555
/>
554556
}
555557
>

apps/webapp/app/components/DevPresence.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function DevPresenceProvider({ children, enabled = true }: DevPresencePro
4242

4343
// Only subscribe to event source if enabled is true
4444
const streamedEvents = useEventSource(
45-
`/resources/orgs/${organization.slug}/projects/${project.slug}/dev/presence`,
45+
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/presence`,
4646
{
4747
event: "presence",
4848
disabled: !enabled,

apps/webapp/app/components/environments/EnvironmentLabel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export function environmentFullTitle(environment: Environment) {
178178
}
179179
}
180180

181-
export function environmentTextClassName(environment: Environment) {
181+
export function environmentTextClassName(environment: { type: Environment["type"] }) {
182182
switch (environment.type) {
183183
case "PRODUCTION":
184184
return "text-prod";

apps/webapp/app/components/navigation/EnvironmentSelector.tsx

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { useFeatures } from "~/hooks/useFeatures";
99
import { useOrganization, type MatchedOrganization } from "~/hooks/useOrganizations";
1010
import { useProject } from "~/hooks/useProject";
1111
import { cn } from "~/utils/cn";
12-
import { branchesPath, docsPath, v3BillingPath } from "~/utils/pathBuilder";
13-
import { EnvironmentCombo, EnvironmentIcon, EnvironmentLabel, environmentFullTitle } from "../environments/EnvironmentLabel";
12+
import { branchesPath, branchesDevPath, docsPath, v3BillingPath } from "~/utils/pathBuilder";
13+
import { EnvironmentCombo, EnvironmentIcon, EnvironmentLabel, environmentFullTitle, environmentTextClassName } from "../environments/EnvironmentLabel";
1414
import { ButtonContent } from "../primitives/Buttons";
1515
import { Header2 } from "../primitives/Headers";
1616
import { Paragraph } from "../primitives/Paragraph";
@@ -111,11 +111,12 @@ export function EnvironmentSelector({
111111
const branchEnvironments = project.environments.filter(
112112
(e) => e.parentEnvironmentId === env.id
113113
);
114+
const allBranchEnvironments = env.type === "DEVELOPMENT" ? [env, ...branchEnvironments] : branchEnvironments;
114115
return (
115116
<Branches
116117
key={env.id}
117118
parentEnvironment={env}
118-
branchEnvironments={branchEnvironments}
119+
branchEnvironments={allBranchEnvironments}
119120
currentEnvironment={environment}
120121
/>
121122
);
@@ -223,11 +224,13 @@ function Branches({
223224
branchEnvironments.length === 0
224225
? "no-branches"
225226
: activeBranches.length === 0
226-
? "no-active-branches"
227-
: "has-branches";
227+
? "no-active-branches"
228+
: "has-branches";
228229

229230
const currentBranchIsArchived = environment.archivedAt !== null;
230231

232+
const envTextClassName = environmentTextClassName(parentEnvironment);
233+
231234
return (
232235
<Popover onOpenChange={(open) => setMenuOpen(open)} open={isMenuOpen}>
233236
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} className="flex">
@@ -260,11 +263,11 @@ function Branches({
260263
to={urlForEnvironment(environment)}
261264
title={
262265
<>
263-
<span className="block w-full text-preview">{environment.branchName}</span>
266+
<span className={cn("block w-full", envTextClassName)}>{environment.branchName}</span>
264267
<Badge variant="extra-small">Archived</Badge>
265268
</>
266269
}
267-
icon={<BranchEnvironmentIconSmall className="size-4 shrink-0 text-preview" />}
270+
icon={<BranchEnvironmentIconSmall className={cn("size-4 shrink-0", envTextClassName)} />}
268271
isSelected={environment.id === currentEnvironment.id}
269272
/>
270273
)}
@@ -276,16 +279,16 @@ function Branches({
276279
<PopoverMenuItem
277280
key={env.id}
278281
to={urlForEnvironment(env)}
279-
title={<span className="block w-full text-preview">{env.branchName}</span>}
280-
icon={<BranchEnvironmentIconSmall className="size-4 shrink-0 text-preview" />}
282+
title={<span className={cn("block w-full", envTextClassName)}>{env.branchName ?? "default"}</span>}
283+
icon={<BranchEnvironmentIconSmall className={cn("size-4 shrink-0", envTextClassName)} />}
281284
isSelected={env.id === currentEnvironment.id}
282285
/>
283286
))}
284287
</>
285288
) : state === "no-branches" ? (
286289
<div className="flex max-w-sm flex-col gap-1 p-2">
287290
<div className="flex items-center gap-1">
288-
<BranchEnvironmentIconSmall className="size-4 text-preview" />
291+
<BranchEnvironmentIconSmall className={cn("size-4", envTextClassName)} />
289292
<Header2>Create your first branch</Header2>
290293
</div>
291294
<Paragraph spacing variant="small">
@@ -305,12 +308,21 @@ function Branches({
305308
)}
306309
</div>
307310
<div className="border-t border-charcoal-700 p-1">
308-
<PopoverMenuItem
309-
to={branchesPath(organization, project, environment)}
310-
title="Manage branches"
311-
icon={<Cog8ToothIcon className="size-4 text-text-dimmed" />}
312-
leadingIconClassName="text-text-dimmed"
313-
/>
311+
{parentEnvironment.type === "DEVELOPMENT" ? (
312+
<PopoverMenuItem
313+
to={branchesDevPath(organization, project, environment)}
314+
title="Manage dev branches"
315+
icon={<Cog8ToothIcon className="size-4 text-text-dimmed" />}
316+
leadingIconClassName="text-text-dimmed"
317+
/>
318+
) : (
319+
<PopoverMenuItem
320+
to={branchesPath(organization, project, environment)}
321+
title="Manage preview branches"
322+
icon={<Cog8ToothIcon className="size-4 text-text-dimmed" />}
323+
leadingIconClassName="text-text-dimmed"
324+
/>
325+
)}
314326
</div>
315327
</PopoverContent>
316328
</div>

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

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,31 @@ export const BranchesOptions = z.object({
8888
page: z.preprocess((val) => Number(val), z.number()).optional(),
8989
});
9090

91+
export const CreateBranchOptions = z.object({
92+
projectId: z.string(),
93+
env: z.enum(["preview", "development"]),
94+
branchName: z.string().min(1),
95+
git: GitMeta.optional(),
96+
});
97+
98+
export const schema = CreateBranchOptions.and(
99+
z.object({
100+
failurePath: z.string(),
101+
})
102+
);
103+
104+
const PurchaseSchema = z.discriminatedUnion("action", [
105+
z.object({
106+
action: z.literal("purchase"),
107+
amount: z.coerce.number().int("Must be a whole number").min(0, "Amount must be 0 or more"),
108+
}),
109+
z.object({
110+
action: z.literal("quota-increase"),
111+
amount: z.coerce.number().int("Must be a whole number").min(1, "Amount must be greater than 0"),
112+
}),
113+
]);
114+
115+
91116
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
92117
const userId = await requireUserId(request);
93118
const { projectParam } = ProjectParamSchema.parse(params);
@@ -101,6 +126,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
101126
const result = await presenter.call({
102127
userId,
103128
projectSlug: projectParam,
129+
env: "preview",
104130
...options,
105131
});
106132

@@ -114,31 +140,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
114140
}
115141
};
116142

117-
export const CreateBranchOptions = z.object({
118-
parentEnvironmentId: z.string(),
119-
branchName: z.string().min(1),
120-
git: GitMeta.optional(),
121-
});
122-
123-
export type CreateBranchOptions = z.infer<typeof CreateBranchOptions>;
124-
125-
export const schema = CreateBranchOptions.and(
126-
z.object({
127-
failurePath: z.string(),
128-
})
129-
);
130-
131-
const PurchaseSchema = z.discriminatedUnion("action", [
132-
z.object({
133-
action: z.literal("purchase"),
134-
amount: z.coerce.number().int("Must be a whole number").min(0, "Amount must be 0 or more"),
135-
}),
136-
z.object({
137-
action: z.literal("quota-increase"),
138-
amount: z.coerce.number().int("Must be a whole number").min(1, "Amount must be greater than 0"),
139-
}),
140-
]);
141-
142143
export async function action({ request, params }: ActionFunctionArgs) {
143144
const userId = await requireUserId(request);
144145

@@ -328,7 +329,7 @@ export default function Page() {
328329
New branch…
329330
</Button>
330331
}
331-
parentEnvironment={branchableEnvironment}
332+
env="preview"
332333
/>
333334
)}
334335
</PageAccessories>
@@ -338,7 +339,7 @@ export default function Page() {
338339
{!hasBranches ? (
339340
<MainCenteredContainer className="max-w-md">
340341
<BranchesNoBranches
341-
parentEnvironment={branchableEnvironment}
342+
envType="preview"
342343
limits={limits}
343344
canUpgrade={canUpgrade ?? false}
344345
showSelfServe={showSelfServe}
@@ -924,17 +925,18 @@ function updateBranchState({
924925

925926
export function NewBranchPanel({
926927
button,
927-
parentEnvironment,
928+
env,
928929
}: {
929930
button: React.ReactNode;
930-
parentEnvironment: { id: string };
931+
env: "preview" | "development";
931932
}) {
933+
const project = useProject();
932934
const lastSubmission = useActionData<typeof action>();
933935
const location = useLocation();
934936
const [searchParams, setSearchParams] = useSearchParams();
935937
const [isOpen, setIsOpen] = useState(false);
936938

937-
const [form, { parentEnvironmentId, branchName, failurePath }] = useForm({
939+
const [form, { projectId, env: envField, branchName, failurePath }] = useForm({
938940
id: "create-branch",
939941
lastSubmission: lastSubmission as any,
940942
onValidate({ formData }) {
@@ -962,8 +964,12 @@ export function NewBranchPanel({
962964
<Form method="post" {...form.props} className="w-full">
963965
<Fieldset className="max-w-full gap-y-3">
964966
<input
965-
value={parentEnvironment.id}
966-
{...conform.input(parentEnvironmentId, { type: "hidden" })}
967+
value={project.id}
968+
{...conform.input(projectId, { type: "hidden" })}
969+
/>
970+
<input
971+
value={env}
972+
{...conform.input(envField, { type: "hidden" })}
967973
/>
968974
<input
969975
value={location.pathname}

0 commit comments

Comments
 (0)