fix: hide header CTA buttons during empty/loading states (#12560)

* fix: hide header CTA buttons during empty/loading states on deployments page

Lift useProviderFilter and useGetDeploymentsByProviders hooks to
DeploymentsPage so the parent can conditionally render the header
button. Hide "New Deployment" when there are no deployments and
"New Environment" when there are no providers. Also hide the
environment dropdown when the deployment list is empty and remove
the unused "no-providers" empty state variant.

* fix: update deployment E2E tests for hidden header buttons in empty state

Adapt Playwright tests to use subtab-deployments as the page-ready
selector instead of new-deployment-btn (now hidden when data is empty).
Use empty-state CTA buttons where header buttons are no longer visible.
Add test for editing tool name on the review step.
This commit is contained in:
Viktor Avelino
2026-04-08 12:10:55 -04:00
committed by GitHub
parent b3666fe554
commit 9be88549e2
8 changed files with 115 additions and 101 deletions

View File

@ -7,9 +7,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useDeleteDeployment } from "@/controllers/API/queries/deployments/use-delete-deployment";
import { useGetDeploymentsByProviders } from "@/controllers/API/queries/deployments/use-get-deployments-by-providers";
import { useDeleteWithConfirmation } from "../hooks/use-delete-with-confirmation";
import { useProviderFilter } from "../hooks/use-provider-filter";
import { useTestDeploymentModal } from "../hooks/use-test-deployment-modal";
import type { Deployment, ProviderAccount } from "../types";
import DeploymentDetailsModal from "./deployment-details-modal/deployment-details-modal";
@ -23,30 +21,26 @@ import TypeToConfirmDeleteDialog from "./type-to-confirm-delete-dialog";
const buildDeploymentDeleteParams = (id: string) => ({ deployment_id: id });
interface DeploymentsContentProps {
isLoadingProviders: boolean;
providers: ProviderAccount[];
deployments: Deployment[];
isLoading: boolean;
selectedProviderId: string;
setSelectedProviderId: (id: string) => void;
providerMap: Record<string, string>;
stepperOpen: boolean;
setStepperOpen: (open: boolean) => void;
onGoToProviders: () => void;
}
export default function DeploymentsContent({
isLoadingProviders,
providers,
deployments,
isLoading,
selectedProviderId,
setSelectedProviderId,
providerMap,
stepperOpen,
setStepperOpen,
onGoToProviders,
}: DeploymentsContentProps) {
const {
selectedProviderId,
setSelectedProviderId,
providerIdsToQuery,
providerMap,
} = useProviderFilter(providers);
const { deployments, isLoading: isLoadingDeployments } =
useGetDeploymentsByProviders(providerIdsToQuery);
const testModal = useTestDeploymentModal();
const { mutate: deleteDeployment } = useDeleteDeployment();
@ -64,25 +58,10 @@ export default function DeploymentsContent({
null,
);
const isLoading = isLoadingProviders || isLoadingDeployments;
const hasProviders = providers.length > 0;
const content = (() => {
if (isLoading) return <DeploymentsLoadingSkeleton />;
if (!hasProviders)
return (
<DeploymentsEmptyState
variant="no-providers"
onAction={onGoToProviders}
/>
);
if (deployments.length === 0)
return (
<DeploymentsEmptyState
variant="no-deployments"
onAction={() => setStepperOpen(true)}
/>
);
return <DeploymentsEmptyState onAction={() => setStepperOpen(true)} />;
return (
<DeploymentsTable
deployments={deployments}
@ -101,7 +80,7 @@ export default function DeploymentsContent({
return (
<>
{providers.length >= 1 && (
{providers.length >= 1 && deployments.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Environment:</span>
<Select

View File

@ -2,46 +2,26 @@ import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
interface DeploymentsEmptyStateProps {
variant: "no-providers" | "no-deployments";
onAction: () => void;
}
const copy = {
"no-providers": {
title: "No Environments",
description: "Add an environment before creating deployments.",
button: "Add Environment",
icon: "Plus" as const,
testId: "add-environment-empty-btn",
},
"no-deployments": {
title: "No Deployments",
description:
"Create your first deployment to run your flows in production.",
button: "Create Deployment",
icon: "Plus" as const,
testId: "create-deployment-empty-btn",
},
};
export default function DeploymentsEmptyState({
variant,
onAction,
}: DeploymentsEmptyStateProps) {
const { title, description, button, icon, testId } = copy[variant];
return (
<div className="flex flex-col items-center justify-center py-24">
<h3 className="text-lg font-semibold">{title}</h3>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
<h3 className="text-lg font-semibold">No Deployments</h3>
<p className="mt-1 text-sm text-muted-foreground">
Create your first deployment to run your flows in production.
</p>
<Button
variant="outline"
className="mt-4"
data-testid={testId}
data-testid="create-deployment-empty-btn"
onClick={onAction}
>
<ForwardedIconComponent name={icon} className="h-4 w-4" />
{button}
<ForwardedIconComponent name="Plus" className="h-4 w-4" />
Create Deployment
</Button>
</div>
);

View File

@ -2,11 +2,13 @@ import { useState } from "react";
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import { useGetProviderAccounts } from "@/controllers/API/queries/deployment-provider-accounts/use-get-provider-accounts";
import { useGetDeploymentsByProviders } from "@/controllers/API/queries/deployments/use-get-deployments-by-providers";
import DeploymentsContent from "./components/deployments-content";
import ProvidersContent from "./components/providers-content";
import SubTabToggle, {
type DeploymentSubTab,
} from "./components/sub-tab-toggle";
import { useProviderFilter } from "./hooks/use-provider-filter";
export default function DeploymentsPage() {
const [activeSubTab, setActiveSubTab] =
@ -18,37 +20,56 @@ export default function DeploymentsPage() {
useGetProviderAccounts({});
const providers = providersData?.provider_accounts ?? [];
const {
selectedProviderId,
setSelectedProviderId,
providerIdsToQuery,
providerMap,
} = useProviderFilter(providers);
const { deployments, isLoading: isLoadingDeployments } =
useGetDeploymentsByProviders(providerIdsToQuery);
const showHeaderButton =
activeSubTab === "providers"
? !isLoadingProviders && providers.length > 0
: !isLoadingProviders && !isLoadingDeployments && deployments.length > 0;
return (
<div className="flex flex-col gap-4 pt-4">
<div className="flex items-center justify-between">
<SubTabToggle activeTab={activeSubTab} onTabChange={setActiveSubTab} />
<Button
onClick={() =>
activeSubTab === "providers"
? setAddProviderOpen(true)
: setStepperOpen(true)
}
data-testid={
activeSubTab === "providers"
? "new-provider-btn"
: "new-deployment-btn"
}
>
<ForwardedIconComponent name="Plus" className="h-4 w-4" />
{activeSubTab === "providers" ? "New Environment" : "New Deployment"}
</Button>
{showHeaderButton && (
<Button
onClick={() =>
activeSubTab === "providers"
? setAddProviderOpen(true)
: setStepperOpen(true)
}
data-testid={
activeSubTab === "providers"
? "new-provider-btn"
: "new-deployment-btn"
}
>
<ForwardedIconComponent name="Plus" className="h-4 w-4" />
{activeSubTab === "providers"
? "New Environment"
: "New Deployment"}
</Button>
)}
</div>
{activeSubTab === "deployments" && (
<DeploymentsContent
isLoadingProviders={isLoadingProviders}
providers={providers}
deployments={deployments}
isLoading={isLoadingProviders || isLoadingDeployments}
selectedProviderId={selectedProviderId}
setSelectedProviderId={setSelectedProviderId}
providerMap={providerMap}
stepperOpen={stepperOpen}
setStepperOpen={setStepperOpen}
onGoToProviders={() => {
setActiveSubTab("providers");
setAddProviderOpen(true);
}}
/>
)}

View File

@ -113,8 +113,8 @@ async function openDeploymentStepper(page: Page) {
await setupDeploymentMocks(page, myCollectionId);
await page.getByTestId("deployments-btn").click();
await page.waitForSelector('[data-testid="new-deployment-btn"]');
await page.getByTestId("new-deployment-btn").click();
await page.waitForSelector('[data-testid="subtab-deployments"]');
await page.getByTestId("create-deployment-empty-btn").click();
// Wait for the stepper dialog to appear
await page.waitForSelector('[data-testid="deployment-stepper-next"]');
}
@ -187,10 +187,10 @@ test(
);
await awaitBootstrapTest(page, { skipModal: true });
await setupDeploymentMocks(page);
await setupDeploymentMocks(page, "");
await page.getByTestId("deployments-btn").click();
await page.waitForSelector('[data-testid="new-deployment-btn"]');
await page.getByTestId("new-deployment-btn").click();
await page.waitForSelector('[data-testid="subtab-deployments"]');
await page.getByTestId("create-deployment-empty-btn").click();
await expect(page.getByTestId("stepper-modal-title")).toBeVisible();
await expect(page.getByTestId("deployment-stepper-next")).toBeVisible();
@ -390,3 +390,40 @@ test(
).toBeVisible({ timeout: 10000 });
},
);
// ---------------------------------------------------------------------------
// Test 7: Review step — user can change tool name
// ---------------------------------------------------------------------------
test(
"deployment-create: user can change tool name on review step",
{ tag: ["@deployment", "@workspace"] },
async ({ page }) => {
test.skip(
process.env.LANGFLOW_FEATURE_WXO_DEPLOYMENTS !== "true",
"Requires LANGFLOW_FEATURE_WXO_DEPLOYMENTS=true",
);
await openDeploymentStepper(page);
await goToStepReview(page);
// The edit tool name button should be visible on the review step
await expect(page.getByTestId("edit-tool-name")).toBeVisible();
// Click the edit (pencil) button to enter editing mode
await page.getByTestId("edit-tool-name").click();
// The tool name input should appear
const toolNameInput = page.getByTestId("tool-name-input");
await expect(toolNameInput).toBeVisible();
// Clear and type a new tool name
await toolNameInput.fill("Custom Tool Name");
// Confirm the change by pressing Enter
await toolNameInput.press("Enter");
// The input should disappear and the new name should be visible
await expect(toolNameInput).not.toBeVisible();
await expect(page.getByText("Custom Tool Name")).toBeVisible();
},
);

View File

@ -70,7 +70,7 @@ async function setupRoutes(page: Parameters<typeof test>[2]["page"]) {
async function navigateToDeployments(page: Parameters<typeof test>[2]["page"]) {
await awaitBootstrapTest(page, { skipModal: true });
await page.getByTestId("deployments-btn").click();
await page.waitForSelector('[data-testid="new-deployment-btn"]');
await page.waitForSelector('[data-testid="subtab-deployments"]');
}
async function openEditDialog(page: Parameters<typeof test>[2]["page"]) {

View File

@ -11,9 +11,8 @@ async function navigateToProvidersTab(
) {
await awaitBootstrapTest(page, { skipModal: true });
await page.getByTestId("deployments-btn").click();
await page.waitForSelector('[data-testid="new-deployment-btn"]');
await page.waitForSelector('[data-testid="subtab-deployments"]');
await page.getByTestId("subtab-providers").click();
await page.waitForSelector('[data-testid="new-provider-btn"]');
}
test(
@ -74,7 +73,7 @@ test(
await navigateToProvidersTab(page);
await page.getByTestId("new-provider-btn").click();
await page.getByTestId("add-provider-empty-btn").click();
await expect(page.getByTestId("add-provider-modal-title")).toBeVisible();
await expect(page.getByTestId("add-provider-save")).toBeDisabled();
@ -108,7 +107,7 @@ test(
await navigateToProvidersTab(page);
await page.getByTestId("new-provider-btn").click();
await page.getByTestId("add-provider-empty-btn").click();
await expect(page.getByTestId("add-provider-save")).toBeDisabled();
@ -167,7 +166,7 @@ test(
await navigateToProvidersTab(page);
await page.getByTestId("new-provider-btn").click();
await page.getByTestId("add-provider-empty-btn").click();
await page.getByPlaceholder("e.g. Production").fill("My Env");
await page

View File

@ -37,7 +37,7 @@ async function setupBaseRoutes(page: Page) {
async function navigateToDeploymentsPage(page: Page) {
await awaitBootstrapTest(page, { skipModal: true });
await page.getByTestId("deployments-btn").click();
await page.waitForSelector('[data-testid="new-deployment-btn"]');
await page.waitForSelector('[data-testid="subtab-deployments"]');
}
test(

View File

@ -11,7 +11,7 @@ async function navigateToDeploymentsTab(
) {
await awaitBootstrapTest(page, { skipModal: true });
await page.getByTestId("deployments-btn").click();
await page.waitForSelector('[data-testid="new-deployment-btn"]');
await page.waitForSelector('[data-testid="subtab-deployments"]');
}
test(
@ -41,7 +41,7 @@ test(
await awaitBootstrapTest(page, { skipModal: true });
await page.getByTestId("deployments-btn").click();
await page.waitForSelector('[data-testid="new-deployment-btn"]');
await page.waitForSelector('[data-testid="subtab-deployments"]');
await expect(page.getByTestId("subtab-deployments")).toBeVisible();
await expect(page.getByTestId("subtab-providers")).toBeVisible();
@ -75,10 +75,9 @@ test(
await awaitBootstrapTest(page, { skipModal: true });
await page.getByTestId("deployments-btn").click();
await page.waitForSelector('[data-testid="new-deployment-btn"]');
await page.waitForSelector('[data-testid="subtab-deployments"]');
await expect(page.getByTestId("add-environment-empty-btn")).toBeVisible();
await expect(page.getByTestId("new-deployment-btn")).toBeVisible();
await expect(page.getByTestId("create-deployment-empty-btn")).toBeVisible();
},
);
@ -109,12 +108,11 @@ test(
await awaitBootstrapTest(page, { skipModal: true });
await page.getByTestId("deployments-btn").click();
await page.waitForSelector('[data-testid="new-deployment-btn"]');
await page.waitForSelector('[data-testid="subtab-deployments"]');
await page.getByTestId("subtab-providers").click();
await expect(page.getByTestId("add-provider-empty-btn")).toBeVisible();
await expect(page.getByTestId("new-provider-btn")).toBeVisible();
},
);
@ -145,7 +143,7 @@ test(
await awaitBootstrapTest(page, { skipModal: true });
await page.getByTestId("deployments-btn").click();
await page.waitForSelector('[data-testid="new-deployment-btn"]');
await page.waitForSelector('[data-testid="subtab-deployments"]');
await expect(page.getByTestId("deployment-row-dep-1")).toBeVisible();
},
@ -178,7 +176,7 @@ test(
await awaitBootstrapTest(page, { skipModal: true });
await page.getByTestId("deployments-btn").click();
await page.waitForSelector('[data-testid="new-deployment-btn"]');
await page.waitForSelector('[data-testid="subtab-deployments"]');
await page.getByTestId("subtab-providers").click();