Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3f422bc
feat: add github section
ogzhanolguncu Feb 16, 2026
4dc8ef4
feat: Add icons
ogzhanolguncu Feb 16, 2026
e0a3f95
Merge branch 'main' of github.com:unkeyed/unkey into deploy-settings
ogzhanolguncu Feb 17, 2026
378bd1c
feat: add new sections
ogzhanolguncu Feb 17, 2026
66d085b
feat: add settingsgroup
ogzhanolguncu Feb 17, 2026
d0f3d61
feat: add region selection
ogzhanolguncu Feb 17, 2026
7b21f5c
feat: add instances
ogzhanolguncu Feb 17, 2026
c7bef6e
feat: add memory and cpu section
ogzhanolguncu Feb 17, 2026
4166c70
feat: add sections
ogzhanolguncu Feb 17, 2026
bb4043e
feat: add health check
ogzhanolguncu Feb 17, 2026
a7e2a42
feat: add scaling
ogzhanolguncu Feb 18, 2026
1016d41
fix: get rid of redundant prop
ogzhanolguncu Feb 18, 2026
065b5fc
refactor: Add toasts to mutations
ogzhanolguncu Feb 18, 2026
16a20da
refactor: rename component
ogzhanolguncu Feb 18, 2026
e2f4192
feat: add port section
ogzhanolguncu Feb 18, 2026
d46a6a0
feat: fix overlapping borders
ogzhanolguncu Feb 18, 2026
b3063d8
refactor: fix healthcheck tRPC
ogzhanolguncu Feb 18, 2026
d0efd8f
feat: add command section
ogzhanolguncu Feb 18, 2026
d2ba71f
feat: add env section
ogzhanolguncu Feb 18, 2026
f4dd76d
fix: finalize env-vars
ogzhanolguncu Feb 18, 2026
cff917e
refactor: finalize
ogzhanolguncu Feb 18, 2026
9d45ef3
feat: Add custom domains
ogzhanolguncu Feb 18, 2026
5080003
fix: overflwo
ogzhanolguncu Feb 18, 2026
e21c5da
feat: make tRPC route for each mutation
ogzhanolguncu Feb 18, 2026
dc226be
fix: displayValue styles
ogzhanolguncu Feb 18, 2026
3aadd23
refactor: tidy
ogzhanolguncu Feb 18, 2026
a7d00f1
fix: revert accidental changes
ogzhanolguncu Feb 18, 2026
9d26195
feat: add cname table
ogzhanolguncu Feb 18, 2026
a00ecb7
fix: github styling issues
ogzhanolguncu Feb 18, 2026
3a9678e
refactor: tidy
ogzhanolguncu Feb 18, 2026
a92fcd0
refactor: rename
ogzhanolguncu Feb 18, 2026
bf69555
fix: linter
ogzhanolguncu Feb 18, 2026
6390202
Merge branch 'main' of github.com:unkeyed/unkey into deploy-settings
ogzhanolguncu Feb 18, 2026
fe3db20
fix: dynamic form issue
ogzhanolguncu Feb 18, 2026
3d35ab3
feat: allow env selection
ogzhanolguncu Feb 18, 2026
1cac683
chore: tidy
ogzhanolguncu Feb 18, 2026
12c7a05
fix: use same chevron
ogzhanolguncu Feb 18, 2026
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 @@ -75,7 +75,7 @@ export const DefaultBytes: React.FC<Props> = ({ keyAuth, apiId }) => {
</div>
}
border="top"
className="border-b"
className="border-b border-grayA-4"
contentWidth="w-full lg:w-[420px] h-full justify-end items-end"
>
<form
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const DeleteProtection: React.FC<Props> = ({ api }) => {
)
}
border="top"
className="border-b"
className="border-b border-grayA-4"
>
<div className="flex w-full gap-2 lg:items-center justify-end">
{api.deleteProtection ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { collection } from "@/lib/collections";
import { type CustomDomain, collection } from "@/lib/collections";
import { cn } from "@/lib/utils";
import {
Button,
Expand All @@ -12,7 +12,6 @@ import {
} from "@unkey/ui";
import { useEffect, useRef, useState } from "react";
import { useProjectData } from "../../data-provider";
import type { CustomDomain } from "./types";

// Basic domain validation regex
const DOMAIN_REGEX = /^(?!:\/\/)([a-zA-Z0-9-_]+\.)+[a-zA-Z]{2,}$/;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { cn } from "@unkey/ui/src/lib/utils";
import { useState } from "react";
import { EmptySection } from "../../components/empty-section";
import { useProjectData } from "../../data-provider";
import { CustomDomainRow } from "../../settings/components/advanced-settings/custom-domains/custom-domain-row";
import { AddCustomDomain } from "./add-custom-domain";
import { CustomDomainRow, CustomDomainRowSkeleton } from "./custom-domain-row";

type CustomDomainsSectionProps = {
environments: Array<{ id: string; slug: string }>;
Expand Down Expand Up @@ -85,3 +85,15 @@ function EmptyState({ onAdd, hasEnvironments }: { onAdd: () => void; hasEnvironm
</EmptySection>
);
}

export function CustomDomainRowSkeleton() {
return (
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-4 last:border-b-0">
<div className="flex items-center gap-3">
<div className="w-4 h-4 bg-gray-4 rounded animate-pulse" />
<div className="w-32 h-4 bg-gray-4 rounded animate-pulse" />
</div>
<div className="w-16 h-5 bg-gray-4 rounded animate-pulse" />
</div>
);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"use client";

import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { SquareTerminal } from "@unkey/icons";
import { FormTextarea, InfoTooltip, toast } from "@unkey/ui";
import { useEffect } from "react";
import { useForm, useWatch } from "react-hook-form";
import { z } from "zod";
import { useProjectData } from "../../../data-provider";
import { FormSettingCard } from "../shared/form-setting-card";

const commandSchema = z.object({
command: z.string(),
});

type CommandFormValues = z.infer<typeof commandSchema>;

export const Command = () => {
const { environments } = useProjectData();
const environmentId = environments[0]?.id;

const { data: settingsData } = trpc.deploy.environmentSettings.get.useQuery(
{ environmentId: environmentId ?? "" },
{ enabled: Boolean(environmentId) },
);

const rawCommand = settingsData?.runtimeSettings?.command as string[] | undefined;
const defaultCommand = (rawCommand ?? []).join(" ");

return <CommandForm environmentId={environmentId ?? ""} defaultCommand={defaultCommand} />;
};
Comment on lines +31 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard submissions when environmentId is missing.

environmentId ?? "" means a user can edit and submit before the ID loads, sending an empty string to the mutation (likely a no-op with a misleading success). Gate the submit and disable save when environmentId is falsy.

🔒 Suggested guard
-  const onSubmit = async (values: CommandFormValues) => {
+  const onSubmit = async (values: CommandFormValues) => {
+    if (!environmentId) return;
     const trimmed = values.command.trim();
     const command = trimmed === "" ? [] : trimmed.split(/\s+/).filter(Boolean);
     await updateCommand.mutateAsync({ environmentId, command });
   };
@@
-      canSave={isValid && !isSubmitting && hasChanges}
+      canSave={isValid && !isSubmitting && hasChanges && Boolean(environmentId)}

Also applies to: 73-97

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/command.tsx
around lines 31 - 32, The component currently passes environmentId ?? "" into
CommandForm which allows submitting while the real environmentId is missing;
update CommandForm usage and its submit path to require a truthy environmentId:
disable the Save button and prevent form submission when environmentId is falsy,
show a loading/disabled state until environmentId is available, and
short-circuit the mutation call in the CommandForm submit handler (or the parent
submit proxy) to avoid calling the mutation with an empty string; touch the
CommandForm component and any submit handler/mutation invocation that reads
environmentId (references: CommandForm, environmentId, the form submit
function/mutation) and apply the same guard for the other occurrences around
lines 73–97.


type CommandFormProps = {
environmentId: string;
defaultCommand: string;
};

const CommandForm: React.FC<CommandFormProps> = ({ environmentId, defaultCommand }) => {
const utils = trpc.useUtils();

const {
register,
handleSubmit,
formState: { isValid, isSubmitting, errors },
control,
reset,
} = useForm<CommandFormValues>({
resolver: zodResolver(commandSchema),
mode: "onChange",
defaultValues: { command: defaultCommand },
});

useEffect(() => {
reset({ command: defaultCommand });
}, [defaultCommand, reset]);

const currentCommand = useWatch({ control, name: "command" });
const hasChanges = currentCommand !== defaultCommand;

const updateCommand = trpc.deploy.environmentSettings.runtime.updateCommand.useMutation({
onSuccess: () => {
toast.success("Command updated");
utils.deploy.environmentSettings.get.invalidate({ environmentId });
},
onError: (err) => {
toast.error("Failed to update command", {
description: err.message,
});
},
});

const onSubmit = async (values: CommandFormValues) => {
const trimmed = values.command.trim();
const command = trimmed === "" ? [] : trimmed.split(/\s+/).filter(Boolean);
await updateCommand.mutateAsync({ environmentId, command });
};

return (
<FormSettingCard
icon={<SquareTerminal className="text-gray-12" iconSize="xl-medium" />}
title="Command"
description="The command to start your application. Changes apply on next deploy."
displayValue={
defaultCommand ? (
<InfoTooltip content={defaultCommand} asChild position={{ side: "bottom" }}>
<span className="font-medium text-gray-12 font-mono text-xs truncate max-w-[100px]">
{defaultCommand}
</span>
</InfoTooltip>
) : (
<span className="text-gray-11 font-normal">Default</span>
)
}
onSubmit={handleSubmit(onSubmit)}
canSave={isValid && !isSubmitting && hasChanges}
isSaving={updateCommand.isLoading || isSubmitting}
>
<FormTextarea
label="Command"
placeholder="~ npm start"
className="w-[480px] [&_textarea]:font-mono"
description="
Overrides the default container startup command. Arguments are split on whitespace. Leave
empty to use the image's default command."
variant={errors.command ? "error" : "default"}
{...register("command")}
/>
</FormSettingCard>
);
};
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"use client";
import { collection } from "@/lib/collections";
import { retryDomainVerification } from "@/lib/collections/deploy/custom-domains";
import {
type CustomDomain,
type VerificationStatus,
retryDomainVerification,
} from "@/lib/collections/deploy/custom-domains";
import { cn } from "@/lib/utils";
import {
CircleCheck,
CircleInfo,
Clock,
Link4,
Refresh3,
Trash,
TriangleWarning,
Expand All @@ -22,11 +25,11 @@ import {
TooltipTrigger,
} from "@unkey/ui";
import { useRef, useState } from "react";
import { useProjectData } from "../../data-provider";
import type { CustomDomain, VerificationStatus } from "./types";
import { useProjectData } from "../../../../data-provider";

type CustomDomainRowProps = {
domain: CustomDomain;
environmentSlug?: string;
};

const statusConfig: Record<
Expand Down Expand Up @@ -55,7 +58,7 @@ const statusConfig: Record<
},
};

export function CustomDomainRow({ domain }: CustomDomainRowProps) {
export function CustomDomainRow({ domain, environmentSlug }: CustomDomainRowProps) {
const { projectId } = useProjectData();
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const [isRetrying, setIsRetrying] = useState(false);
Expand All @@ -77,21 +80,25 @@ export function CustomDomainRow({ domain }: CustomDomainRowProps) {
};

return (
<div className="border-b border-gray-4 last:border-b-0 group hover:bg-gray-2 transition-colors">
<div className="border-b border-gray-4 last:border-b-0 group hover:bg-grayA-3 transition-colors">
<div className="flex items-center justify-between px-4 py-3 h-12">
<div className="flex items-center gap-3 flex-1 min-w-0">
<Link4 className="text-gray-9 !size-[14px] flex-shrink-0" />
<a
href={`https://${domain.domain}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-content font-medium hover:underline truncate"
className="text-[13px] text-gray-12 font-medium hover:underline truncate"
>
{domain.domain}
</a>
{environmentSlug && (
<span className="text-[11px] text-gray-11 bg-gray-3 px-1.5 py-0.5 rounded font-mono flex-shrink-0">
{environmentSlug}
</span>
)}
</div>

<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<Badge variant={status.color} className="gap-1">
{status.icon}
{status.label}
Expand Down Expand Up @@ -122,15 +129,15 @@ export function CustomDomainRow({ domain }: CustomDomainRowProps) {
<TooltipContent className="max-w-xs">{domain.verificationError}</TooltipContent>
</Tooltip>
)}

<Button
ref={deleteButtonRef}
size="icon"
variant="outline"
type="button"
variant="ghost"
size="sm"
className="w-7 px-0 justify-center text-error-11 hover:text-error-11 transition-opacity duration-150"
onClick={() => setIsConfirmOpen(true)}
className="size-7 text-gray-9 hover:text-error-9"
ref={deleteButtonRef}
>
<Trash className="!size-[14px]" />
<Trash iconSize="sm-regular" />
</Button>

{deleteButtonRef.current && (
Expand Down Expand Up @@ -180,7 +187,7 @@ function DnsRecordTable({
const txtRecordValue = `unkey-domain-verify=${verificationToken}`;

return (
<div className="px-4 pb-3 space-y-4">
<div className="px-4 pb-3 space-y-3">
<p className="text-xs text-gray-9">Add both DNS records below at your domain provider.</p>

{/* TXT Record (Ownership Verification) */}
Expand All @@ -193,7 +200,7 @@ function DnsRecordTable({
/>
</div>
<div className="border border-gray-4 rounded-lg overflow-hidden text-xs">
<div className="grid grid-cols-[80px_1fr_1fr_60px] bg-gray-3 px-3 py-1.5 text-gray-9 font-medium">
<div className="grid grid-cols-[80px_1fr_1fr_60px] bg-grayA-3 px-3 py-1.5 text-gray-9 font-medium">
<span>Type</span>
<span>Name</span>
<span>Value</span>
Expand Down Expand Up @@ -230,7 +237,7 @@ function DnsRecordTable({
/>
</div>
<div className="border border-gray-4 rounded-lg overflow-hidden text-xs">
<div className="grid grid-cols-[80px_1fr_1fr_60px] bg-gray-3 px-3 py-1.5 text-gray-9 font-medium">
<div className="grid grid-cols-[80px_1fr_1fr_60px] bg-grayA-3 px-3 py-1.5 text-gray-9 font-medium">
<span>Type</span>
<span>Name</span>
<span>Value</span>
Expand Down Expand Up @@ -268,15 +275,3 @@ function StatusIndicator({ verified, label }: { verified: boolean; label: string
</Badge>
);
}

export function CustomDomainRowSkeleton() {
return (
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-4 last:border-b-0">
<div className="flex items-center gap-3">
<div className="w-4 h-4 bg-gray-4 rounded animate-pulse" />
<div className="w-32 h-4 bg-gray-4 rounded animate-pulse" />
</div>
<div className="w-16 h-5 bg-gray-4 rounded animate-pulse" />
</div>
);
}
Loading