mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-27 09:45:05 +08:00
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:
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -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();
|
||||
},
|
||||
);
|
||||
|
||||
@ -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"]) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user