diff --git a/svc/ctrl/worker/deploy/deploy_handler.go b/svc/ctrl/worker/deploy/deploy_handler.go
index c6d678371f9..0ccfc0177e2 100644
--- a/svc/ctrl/worker/deploy/deploy_handler.go
+++ b/svc/ctrl/worker/deploy/deploy_handler.go
@@ -14,7 +14,6 @@ import (
"github.com/unkeyed/unkey/pkg/db"
"github.com/unkeyed/unkey/pkg/fault"
"github.com/unkeyed/unkey/pkg/logger"
- "github.com/unkeyed/unkey/pkg/ptr"
"github.com/unkeyed/unkey/pkg/uid"
githubclient "github.com/unkeyed/unkey/svc/ctrl/worker/github"
)
@@ -58,8 +57,10 @@ const (
//
// Returns terminal errors for validation failures and retryable errors for
// transient system failures.
-func (w *Workflow) Deploy(ctx restate.WorkflowSharedContext, req *hydrav1.DeployRequest) (*hydrav1.DeployResponse, error) {
+func (w *Workflow) Deploy(ctx restate.WorkflowSharedContext, req *hydrav1.DeployRequest) (_ *hydrav1.DeployResponse, retErr error) {
finishedSuccessfully := false
+ var currentStep *db.DeploymentStepsStep
+ var failureReason string
err := assert.All(
assert.NotEmpty(req.GetDeploymentId(), "deployment_id is required"),
@@ -80,14 +81,29 @@ func (w *Workflow) Deploy(ctx restate.WorkflowSharedContext, req *hydrav1.Deploy
if err = w.startDeploymentStep(ctx, deployment, db.DeploymentStepsStepQueued); err != nil {
return nil, err
}
+ queuedStep := db.DeploymentStepsStepQueued
+ currentStep = &queuedStep
defer func() {
if finishedSuccessfully {
return
}
- if err = w.updateDeploymentStatus(ctx, deployment.ID, db.DeploymentsStatusFailed); err != nil {
- logger.Error("deployment failed but we can not set the status", "error", err.Error())
+ if currentStep != nil {
+ msg := failureReason
+ if msg == "" {
+ msg = fault.UserFacingMessage(retErr)
+ }
+ if msg == "" {
+ msg = fmt.Sprintf("Deployment failed during %s step", string(*currentStep))
+ }
+ if stepErr := w.endDeploymentStep(ctx, deployment.ID, *currentStep, &msg); stepErr != nil {
+ logger.Error("failed to end deployment step on failure", "step", *currentStep, "error", stepErr)
+ }
+ }
+
+ if statusErr := w.updateDeploymentStatus(ctx, deployment.ID, db.DeploymentsStatusFailed); statusErr != nil {
+ logger.Error("deployment failed but we can not set the status", "error", statusErr.Error())
}
}()
@@ -137,10 +153,13 @@ func (w *Workflow) Deploy(ctx restate.WorkflowSharedContext, req *hydrav1.Deploy
if err = w.endDeploymentStep(ctx, deployment.ID, db.DeploymentStepsStepQueued, nil); err != nil {
return nil, err
}
+ currentStep = nil
if err = w.startDeploymentStep(ctx, deployment, db.DeploymentStepsStepBuilding); err != nil {
return nil, err
}
+ buildingStep := db.DeploymentStepsStepBuilding
+ currentStep = &buildingStep
dockerImage := ""
@@ -202,9 +221,8 @@ func (w *Workflow) Deploy(ctx restate.WorkflowSharedContext, req *hydrav1.Deploy
WorkspaceID: deployment.WorkspaceID,
})
if err != nil {
- buildErr := fmt.Errorf("failed to build docker image from git: %w", err)
- _ = w.endDeploymentStep(ctx, deployment.ID, db.DeploymentStepsStepBuilding, ptr.P(fault.UserFacingMessage(err)))
- return nil, buildErr
+ failureReason = fault.UserFacingMessage(err)
+ return nil, fmt.Errorf("failed to build docker image from git: %w", err)
}
dockerImage = build.ImageName
@@ -237,6 +255,7 @@ func (w *Workflow) Deploy(ctx restate.WorkflowSharedContext, req *hydrav1.Deploy
if err = w.endDeploymentStep(ctx, deployment.ID, db.DeploymentStepsStepBuilding, nil); err != nil {
return nil, err
}
+ currentStep = nil
if err = w.updateDeploymentStatus(ctx, deployment.ID, db.DeploymentsStatusDeploying); err != nil {
return nil, err
@@ -245,6 +264,8 @@ func (w *Workflow) Deploy(ctx restate.WorkflowSharedContext, req *hydrav1.Deploy
if err = w.startDeploymentStep(ctx, deployment, db.DeploymentStepsStepDeploying); err != nil {
return nil, err
}
+ deployingStep := db.DeploymentStepsStepDeploying
+ currentStep = &deployingStep
// Read region config from runtime settings to determine per-region replica counts.
// If regionConfig is empty, deploy to all available regions with 1 replica each (default).
@@ -309,6 +330,9 @@ func (w *Workflow) Deploy(ctx restate.WorkflowSharedContext, req *hydrav1.Deploy
existingSentinels, err := restate.Run(ctx, func(runCtx restate.RunContext) ([]db.Sentinel, error) {
return db.Query.FindSentinelsByEnvironmentID(runCtx, w.db.RO(), environment.ID)
}, restate.WithName("find existing sentinels"))
+ if err != nil {
+ return nil, fmt.Errorf("failed to find existing sentinels: %w", err)
+ }
existingSentinelsByRegion := make(map[string]db.Sentinel)
for _, sentinel := range existingSentinels {
@@ -417,10 +441,13 @@ func (w *Workflow) Deploy(ctx restate.WorkflowSharedContext, req *hydrav1.Deploy
if err = w.endDeploymentStep(ctx, deployment.ID, db.DeploymentStepsStepDeploying, nil); err != nil {
return nil, err
}
+ currentStep = nil
if err = w.startDeploymentStep(ctx, deployment, db.DeploymentStepsStepNetwork); err != nil {
return nil, err
}
+ networkStep := db.DeploymentStepsStepNetwork
+ currentStep = &networkStep
allDomains := buildDomains(
workspace.Slug,
@@ -500,6 +527,7 @@ func (w *Workflow) Deploy(ctx restate.WorkflowSharedContext, req *hydrav1.Deploy
if err = w.endDeploymentStep(ctx, deployment.ID, db.DeploymentStepsStepNetwork, nil); err != nil {
return nil, err
}
+ currentStep = nil
if err = w.updateDeploymentStatus(ctx, deployment.ID, db.DeploymentsStatusReady); err != nil {
return nil, err
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/page.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/page.tsx
index 255743a5b9a..b4f7d9513ff 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/page.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/page.tsx
@@ -1,11 +1,8 @@
"use client";
+import { PRODUCT_HOME_ROUTES, useProductSelection } from "@/hooks/use-product-selection";
+import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
-import {
- PRODUCT_HOME_ROUTES,
- useProductSelection,
-} from "@/hooks/use-product-selection";
-import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation";
export default function WorkspacePage() {
const router = useRouter();
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/build-step-logs-expanded.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/build-step-logs-expanded.tsx
index a0035c393e3..e34dfebb32f 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/build-step-logs-expanded.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/build-step-logs-expanded.tsx
@@ -33,8 +33,8 @@ export function BuildStepLogsExpanded({ step }: { step: BuildStepRow }) {
/>
|
-
-
+ |
+
{log.message}
|
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/columns.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/columns.tsx
index c2b271eb8d1..2b7e0066349 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/columns.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/columns.tsx
@@ -32,7 +32,7 @@ export const buildStepsColumns: Column[] = [
key: "started_at",
width: "85px",
render: (step) => (
-
+
[] = [
width: "32px",
render: (step) => {
if (step.error) {
- return ;
+ return ;
}
if (step.cached) {
return (
-
+
);
}
@@ -63,7 +63,7 @@ export const buildStepsColumns: Column[] = [
width: "250px",
render: (step) => (
{step.name}
@@ -78,7 +78,7 @@ export const buildStepsColumns: Column
[] = [
return null;
}
return (
-
+
{step.error}
);
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress.tsx
index b627047be57..c84f53fd079 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress.tsx
@@ -2,11 +2,12 @@
import { trpc } from "@/lib/trpc/client";
import { CloudUp, Earth, Hammer2, LayerFront } from "@unkey/icons";
-import { SettingCardGroup } from "@unkey/ui";
+import { Button, SettingCardGroup } from "@unkey/ui";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { DeploymentDomainsCard } from "../../../../components/deployment-domains-card";
import { useProjectData } from "../../../data-provider";
+import { RedeployDialog } from "../../components/table/components/actions/redeploy-dialog";
import { useDeployment } from "../layout-provider";
import { DeploymentBuildStepsTable } from "./build-steps-table/deployment-build-steps-table";
import { DeploymentStep } from "./deployment-step";
@@ -17,6 +18,7 @@ export function DeploymentProgress() {
const params = useParams();
const workspaceSlug = params.workspaceSlug as string;
const projectId = params.projectId as string;
+ const isFailed = deployment.status === "failed";
const steps = trpc.deploy.deployment.steps.useQuery(
{
@@ -33,7 +35,7 @@ export function DeploymentProgress() {
includeStepLogs: true,
},
{
- refetchInterval: 1000,
+ refetchInterval: 1_000,
},
);
@@ -41,13 +43,18 @@ export function DeploymentProgress() {
const [now, setNow] = useState(0);
useEffect(() => {
+ if (isFailed) {
+ return;
+ }
const interval = setInterval(() => setNow(Date.now()), 500);
return () => {
clearInterval(interval);
};
- }, []);
+ }, [isFailed]);
+
const { building, deploying, network, queued } = steps.data ?? {};
+ const [redeployOpen, setRedeployOpen] = useState(false);
const domainsForDeployment = getDomainsForDeployment(deployment.id);
// Latch true once we observe the build actively in progress; stays true after it completes
@@ -55,7 +62,7 @@ export function DeploymentProgress() {
if (building && !building.endedAt) {
hasFreshBuild.current = true;
}
- const isPrebuilt = !hasFreshBuild.current;
+ const isPrebuilt = !hasFreshBuild.current && !building?.error;
useEffect(() => {
if (network?.completed) {
@@ -126,7 +133,9 @@ export function DeploymentProgress() {
? deploying.endedAt
? (deploying.error ?? "Deployed to all machines")
: "Deploying to all machines"
- : "Waiting for build"
+ : isFailed
+ ? "Skipped"
+ : "Waiting for build"
}
duration={deploying ? (deploying.endedAt ?? now) - deploying.startedAt : undefined}
status={
@@ -136,7 +145,9 @@ export function DeploymentProgress() {
? "completed"
: deploying
? "started"
- : "pending"
+ : isFailed
+ ? "skipped"
+ : "pending"
}
/>
+ {isFailed && (
+
+
+
+
+ Deployment failed
+
+ {[queued, building, deploying, network].find((s) => s?.error)?.error ??
+ "Deployment failed"}
+
+
+
+
+
+
setRedeployOpen(false)}
+ selectedDeployment={deployment}
+ />
+
+ )}
{network?.completed && (
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-step.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-step.tsx
index bb77f1ecc38..97b50d38dd4 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-step.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-step.tsx
@@ -10,7 +10,7 @@ type DeploymentStepProps = {
title: string;
description: string;
duration?: number;
- status: "pending" | "started" | "completed" | "error";
+ status: "pending" | "started" | "completed" | "error" | "skipped";
expandable?: React.ReactNode;
defaultExpanded?: boolean;
badgeText?: string;
@@ -25,61 +25,79 @@ export function DeploymentStep({
expandable,
defaultExpanded,
}: DeploymentStepProps) {
- const showGlow = status === "started";
+ const showGlow = status === "started" || status === "error";
+ const isError = status === "error";
+ const isSkipped = status === "skipped";
return (
-
-
-
- }
- title={
-
- {title}
-
+ {title}
+ {isError ? (
+
+ Failed
+
+ ) : (
+
+ Complete
+
)}
- >
- Complete
-
+
+ }
+ iconClassName={showGlow ? "bg-transparent shadow-none dark:ring-0" : undefined}
+ className="relative"
+ description={description}
+ expandable={expandable}
+ defaultExpanded={defaultExpanded}
+ contentWidth="w-fit"
+ >
+
+ {duration ? ms(duration) : null}
+ {status === "completed" ? (
+
+ ) : status === "started" ? (
+
+ ) : status === "error" ? (
+
+ ) : null}
- }
- iconClassName={showGlow ? "bg-transparent shadow-none dark:ring-0" : undefined}
- className="relative"
- description={description}
- expandable={expandable}
- defaultExpanded={defaultExpanded}
- contentWidth="w-fit"
- >
-
- {duration ? ms(duration) : null}
- {status === "completed" ? (
-
- ) : status === "started" ? (
-
- ) : status === "error" ? (
-
- ) : null}
-
-
+
+
);
}
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/actions/deployment-list-table-action.popover.constants.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/actions/deployment-list-table-action.popover.constants.tsx
index 6092cb2340a..be9baf0a9de 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/actions/deployment-list-table-action.popover.constants.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/actions/deployment-list-table-action.popover.constants.tsx
@@ -36,7 +36,8 @@ export const DeploymentListTableActions = ({
selectedDeployment.status === "ready" &&
selectedDeployment.id !== liveDeployment.id;
- const canRedeploy = selectedDeployment.status === "ready";
+ const canRedeploy =
+ selectedDeployment.status === "ready" || selectedDeployment.status === "failed";
return [
{
@@ -95,9 +96,7 @@ export const DeploymentListTableActions = ({
label: "Go to logs...",
icon: ,
onClick: () => {
- router.push(
- `/${workspace.slug}/projects/${selectedDeployment.projectId}/deployments/${selectedDeployment.id}/logs`,
- );
+ router.push(`/${workspace.slug}/projects/${selectedDeployment.projectId}/logs`);
},
},
];
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/deployment-status-badge.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/deployment-status-badge.tsx
index 9ec8da7a585..2f6de02bc93 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/deployment-status-badge.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/deployment-status-badge.tsx
@@ -86,7 +86,7 @@ export const DeploymentStatusBadge = ({ status, className }: DeploymentStatusBad
return (
{
const isLive = liveDeploymentId === deployment.id;
const iconContainer =
;
return (
-
+
{iconContainer}
@@ -133,7 +133,7 @@ export const DeploymentsList = () => {
const mem = isFailed ? null : formatMemoryParts(deployment.memoryMib);
return (
-
+
{isFailed ? (
—
) : (
@@ -146,20 +146,24 @@ export const DeploymentsList = () => {
)}
- {!isFailed && cpu && mem && (
-
-
-
- {cpu.value}
- {cpu.unit}
-
-
-
- {mem.value}
- {mem.unit}
-
-
- )}
+
+ {isFailed || !cpu || !mem ? (
+
—
+ ) : (
+ <>
+
+
+ {cpu.value}
+ {cpu.unit}
+
+
+
+ {mem.value}
+ {mem.unit}
+
+ >
+ )}
+
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/dockerfile-settings.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/dockerfile-settings.tsx
index 3a945c5ed49..66be2449d47 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/dockerfile-settings.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/dockerfile-settings.tsx
@@ -12,7 +12,7 @@ const dockerfileSchema = z.object({
});
export const Dockerfile = () => {
- const { settings } = useEnvironmentSettings();
+ const { settings, autoSave } = useEnvironmentSettings();
const { environmentId, dockerfile: defaultValue } = settings;
const {
@@ -48,6 +48,7 @@ export const Dockerfile = () => {
displayValue={defaultValue}
onSubmit={handleSubmit(onSubmit)}
saveState={saveState}
+ autoSave={autoSave}
>
{
- const { settings } = useEnvironmentSettings();
+ const { settings, autoSave } = useEnvironmentSettings();
const { environmentId, dockerContext: defaultValue } = settings;
const {
@@ -48,6 +48,7 @@ export const RootDirectory = () => {
displayValue={defaultValue || "."}
onSubmit={handleSubmit(onSubmit)}
saveState={saveState}
+ autoSave={autoSave}
>
;
export const Command = () => {
- const { settings } = useEnvironmentSettings();
+ const { settings, autoSave } = useEnvironmentSettings();
const { command, environmentId } = settings;
const defaultCommand = command.join(" ");
@@ -70,6 +70,7 @@ export const Command = () => {
}
onSubmit={handleSubmit(onSubmit)}
saveState={saveState}
+ autoSave={autoSave}
>
;
export const Cpu = () => {
- const { settings } = useEnvironmentSettings();
+ const { settings, autoSave } = useEnvironmentSettings();
const { environmentId, cpuMillicores: defaultCpu } = settings;
const {
@@ -83,6 +83,7 @@ export const Cpu = () => {
})()}
onSubmit={handleSubmit(onSubmit)}
saveState={saveState}
+ autoSave={autoSave}
>
CPU per instance
@@ -97,6 +98,7 @@ export const Cpu = () => {
setValue("cpu", indexToValue(CPU_OPTIONS, value, 256), { shouldValidate: true });
}
}}
+ onValueCommit={autoSave ? () => handleSubmit(onSubmit)() : undefined}
className="flex-1 max-w-[480px]"
rangeStyle={{
background: "linear-gradient(to right, hsla(var(--infoA-4)), hsla(var(--infoA-12)))",
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/healthcheck/index.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/healthcheck/index.tsx
index 040a420eb8c..aec22ed73f1 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/healthcheck/index.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/healthcheck/index.tsx
@@ -20,7 +20,7 @@ import { HTTP_METHODS, type HealthcheckFormValues, healthcheckSchema } from "./s
import { intervalToSeconds, secondsToInterval } from "./utils";
export const Healthcheck = () => {
- const { settings } = useEnvironmentSettings();
+ const { settings, autoSave } = useEnvironmentSettings();
const { healthcheck, environmentId } = settings;
const defaultValues: HealthcheckFormValues = {
@@ -94,6 +94,7 @@ export const Healthcheck = () => {
}
onSubmit={handleSubmit(onSubmit)}
saveState={saveState}
+ autoSave={autoSave}
>
{/* TODO: multi-check when API supports
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/instances.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/instances.tsx
index 09979aeca1b..1fa1d272a30 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/instances.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/instances.tsx
@@ -20,7 +20,7 @@ const instancesSchema = z.object({
type InstancesFormValues = z.infer
;
export const Instances = () => {
- const { settings } = useEnvironmentSettings();
+ const { settings, autoSave } = useEnvironmentSettings();
const { environmentId, regionConfig } = settings;
const selectedRegions = Object.keys(regionConfig);
@@ -82,6 +82,7 @@ export const Instances = () => {
}
onSubmit={handleSubmit(onSubmit)}
saveState={saveState}
+ autoSave={autoSave}
>
Instances per region
@@ -96,6 +97,15 @@ export const Instances = () => {
setValue("instances", value, { shouldValidate: true });
}
}}
+ onValueCommit={
+ autoSave
+ ? ([value]) => {
+ if (value !== undefined) {
+ handleSubmit(onSubmit)();
+ }
+ }
+ : undefined
+ }
className="flex-1 max-w-[480px]"
rangeStyle={{
background:
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx
index a6ab7a084bf..d5eeafd8b9f 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx
@@ -31,7 +31,7 @@ const memorySchema = z.object({
type MemoryFormValues = z.infer
;
export const Memory = () => {
- const { settings } = useEnvironmentSettings();
+ const { settings, autoSave } = useEnvironmentSettings();
const { memoryMib: defaultMemory, environmentId } = settings;
const {
@@ -83,6 +83,7 @@ export const Memory = () => {
})()}
onSubmit={handleSubmit(onSubmit)}
saveState={saveState}
+ autoSave={autoSave}
>
Memory per instance
@@ -99,6 +100,7 @@ export const Memory = () => {
});
}
}}
+ onValueCommit={autoSave ? () => handleSubmit(onSubmit)() : undefined}
className="flex-1 max-w-[480px]"
rangeStyle={{
background:
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/port-settings.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/port-settings.tsx
index d64afab86fa..b8b9f017d23 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/port-settings.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/port-settings.tsx
@@ -12,7 +12,7 @@ const portSchema = z.object({
});
export const Port = () => {
- const { settings } = useEnvironmentSettings();
+ const { settings, autoSave } = useEnvironmentSettings();
const { environmentId, port: defaultValue } = settings;
const {
@@ -48,6 +48,7 @@ export const Port = () => {
displayValue={String(defaultValue)}
onSubmit={handleSubmit(onSubmit)}
saveState={saveState}
+ autoSave={autoSave}
>
;
export const Regions = () => {
- const { settings } = useEnvironmentSettings();
+ const { settings, autoSave } = useEnvironmentSettings();
const { environmentId, regionConfig } = settings;
const defaultRegions = Object.keys(regionConfig);
@@ -35,6 +35,7 @@ export const Regions = () => {
environmentId={environmentId}
defaultRegions={defaultRegions}
availableRegions={availableRegions ?? []}
+ autoSave={autoSave}
/>
);
};
@@ -43,12 +44,14 @@ type RegionsFormProps = {
environmentId: string;
defaultRegions: string[];
availableRegions: string[];
+ autoSave?: boolean;
};
const RegionsForm: React.FC = ({
environmentId,
defaultRegions,
availableRegions,
+ autoSave,
}) => {
const {
handleSubmit,
@@ -150,6 +153,7 @@ const RegionsForm: React.FC = ({
displayValue={displayValue}
onSubmit={handleSubmit(onSubmit)}
saveState={saveState}
+ autoSave={autoSave}
>
;
export const Keyspaces = () => {
- const { settings } = useEnvironmentSettings();
+ const { settings, autoSave } = useEnvironmentSettings();
const { environmentId } = settings;
const { data: availableKeyspaces } =
@@ -39,6 +39,7 @@ export const Keyspaces = () => {
environmentId={environmentId}
defaultKeyspaceIds={defaultKeyspaceIds}
availableKeyspaces={availableKeyspaces ?? {}}
+ autoSave={autoSave}
/>
);
};
@@ -47,12 +48,14 @@ type KeyspacesFormProps = {
environmentId: string;
defaultKeyspaceIds: string[];
availableKeyspaces: Record;
+ autoSave?: boolean;
};
const KeyspacesForm: React.FC = ({
environmentId,
defaultKeyspaceIds,
availableKeyspaces,
+ autoSave,
}) => {
const {
handleSubmit,
@@ -160,6 +163,7 @@ const KeyspacesForm: React.FC = ({
displayValue={displayValue}
onSubmit={handleSubmit(onSubmit)}
saveState={saveState}
+ autoSave={autoSave}
>
;
className?: string;
+ autoSave?: boolean;
};
export const FormSettingCard = ({
@@ -31,6 +32,7 @@ export const FormSettingCard = ({
saveState,
ref,
className,
+ autoSave,
}: EditableSettingCardProps) => {
return (
{
+ if (!autoSave || saveState.status !== "ready") {
+ return;
+ }
+ const relatedTarget = e.relatedTarget instanceof Node ? e.relatedTarget : null;
+ if (!e.currentTarget.contains(relatedTarget)) {
+ e.currentTarget.requestSubmit();
+ }
+ }}
>
-
+
{children}
-
-
-
-
-
+
+
+
+ )}
}
>
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/settings-group.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/settings-group.tsx
index 15737428294..95e6162747b 100644
--- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/settings-group.tsx
+++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/settings-group.tsx
@@ -1,7 +1,7 @@
"use client";
import { ChevronRight } from "@unkey/icons";
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
type SettingsGroupProps = {
icon: React.ReactNode;
@@ -20,6 +20,10 @@ export const SettingsGroup = ({
}: SettingsGroupProps) => {
const [expanded, setExpanded] = useState(defaultExpanded);
+ useEffect(() => {
+ setExpanded(defaultExpanded);
+ }, [defaultExpanded]);
+
return (
@@ -30,7 +34,7 @@ export const SettingsGroup = ({