Skip to content

Commit fb1e5f7

Browse files
Merge branch 'release-1.10.0' into LE-1080
2 parents 66552e8 + 61c1d18 commit fb1e5f7

8 files changed

Lines changed: 220 additions & 27 deletions

File tree

src/backend/tests/unit/test_logger.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
setup_gunicorn_logger,
3434
setup_uvicorn_logger,
3535
)
36+
from loguru import logger as loguru_logger
3637

3738

3839
class TestConfigure:
@@ -104,6 +105,29 @@ def test_configure_with_log_file(self):
104105
if isinstance(handler, logging.handlers.RotatingFileHandler):
105106
logging.root.removeHandler(handler)
106107

108+
def test_configure_routes_loguru_messages_to_log_file(self):
109+
"""Test configure() routes Loguru messages through Langflow logging."""
110+
with tempfile.TemporaryDirectory() as tmp_dir:
111+
log_file_path = Path(tmp_dir) / "langflow.log"
112+
113+
for handler in logging.root.handlers[:]:
114+
if isinstance(handler, logging.handlers.RotatingFileHandler):
115+
logging.root.removeHandler(handler)
116+
117+
try:
118+
configure(log_level="INFO", log_file=log_file_path, cache=False)
119+
loguru_logger.info("Custom component log message")
120+
121+
for handler in logging.root.handlers:
122+
if hasattr(handler, "flush"):
123+
handler.flush()
124+
125+
assert "Custom component log message" in log_file_path.read_text()
126+
finally:
127+
for handler in logging.root.handlers[:]:
128+
if isinstance(handler, logging.handlers.RotatingFileHandler):
129+
logging.root.removeHandler(handler)
130+
107131
def test_configure_with_invalid_log_file_path(self):
108132
"""Test configure() with invalid log file path falls back to cache dir."""
109133
invalid_path = Path("/nonexistent/directory/log.txt")

src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/step-attach-flows-version-panel.test.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,13 @@ const defaultProps = {
5959
selectedFlow: selectedFlow as FlowType | undefined,
6060
versions,
6161
isLoadingVersions: false,
62+
isCreatingDraftVersion: false,
6263
selectedVersionByFlow: new Map<
6364
string,
6465
{ versionId: string; versionTag: string }
6566
>(),
6667
onAttach: jest.fn(),
68+
onCreateFromDraft: jest.fn(),
6769
};
6870

6971
function renderPanel(overrides: Partial<typeof defaultProps> = {}) {
@@ -106,7 +108,21 @@ describe("Loading state", () => {
106108
describe("No versions found", () => {
107109
it("shows empty state when no versions exist", () => {
108110
renderPanel({ versions: [] });
109-
expect(screen.getByText("No versions found")).toBeInTheDocument();
111+
expect(
112+
screen.getByText(
113+
"Deploy this flow by creating a version from current Draft",
114+
),
115+
).toBeInTheDocument();
116+
expect(screen.getByTestId("create-version-from-draft")).toBeInTheDocument();
117+
});
118+
119+
it("calls onCreateFromDraft when clicking empty-state CTA", async () => {
120+
const user = userEvent.setup();
121+
const onCreateFromDraft = jest.fn();
122+
renderPanel({ versions: [], onCreateFromDraft });
123+
124+
await user.click(screen.getByTestId("create-version-from-draft"));
125+
expect(onCreateFromDraft).toHaveBeenCalled();
110126
});
111127
});
112128

src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/step-attach-flows.test.tsx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ let mockVersionsData: {
102102
}>;
103103
} | null = null;
104104
let mockIsLoadingVersions = false;
105+
const mockCreateSnapshot = jest.fn();
105106

106107
jest.mock(
107108
"@/controllers/API/queries/flow-version/use-get-flow-versions",
@@ -113,6 +114,16 @@ jest.mock(
113114
}),
114115
);
115116

117+
jest.mock(
118+
"@/controllers/API/queries/flow-version/use-post-create-snapshot",
119+
() => ({
120+
usePostCreateSnapshot: () => ({
121+
mutateAsync: mockCreateSnapshot,
122+
isPending: false,
123+
}),
124+
}),
125+
);
126+
116127
const mockDetectEnvVars = jest.fn().mockResolvedValue({ variables: [] });
117128

118129
jest.mock(
@@ -223,6 +234,10 @@ beforeEach(() => {
223234
],
224235
};
225236
mockIsLoadingVersions = false;
237+
mockCreateSnapshot.mockResolvedValue({
238+
id: "ver-new",
239+
version_tag: "v3",
240+
});
226241
});
227242

228243
// ---------------------------------------------------------------------------
@@ -304,7 +319,33 @@ describe("Version panel", () => {
304319
it("shows empty state when no versions available", () => {
305320
mockVersionsData = { entries: [] };
306321
render(<StepAttachFlows />);
307-
expect(screen.getByText("No versions found")).toBeInTheDocument();
322+
expect(
323+
screen.getByText(
324+
"Deploy this flow by creating a version from current Draft",
325+
),
326+
).toBeInTheDocument();
327+
});
328+
329+
it("creates version from draft and opens connection panel", async () => {
330+
const user = userEvent.setup();
331+
mockVersionsData = { entries: [] };
332+
render(<StepAttachFlows />);
333+
334+
await user.click(screen.getByTestId("create-version-from-draft"));
335+
336+
await waitFor(() => {
337+
expect(mockCreateSnapshot).toHaveBeenCalledWith({ flowId: "flow-1" });
338+
});
339+
340+
await waitFor(() => {
341+
expect(
342+
screen.getByText("Select or Create New Connection"),
343+
).toBeInTheDocument();
344+
});
345+
346+
expect(mockDetectEnvVars).toHaveBeenCalledWith({
347+
flow_version_ids: ["ver-new"],
348+
});
308349
});
309350

310351
it("shows ATTACHED badge for already-attached versions", () => {

src/frontend/src/pages/MainPage/pages/deploymentsPage/__tests__/step-type.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,20 @@ describe("Name input", () => {
241241
);
242242
});
243243

244+
it("does not show available state when format error is active", () => {
245+
mockDeploymentName = "1";
246+
mockHasDeploymentNameFormatError = true;
247+
248+
render(<StepType />);
249+
250+
expect(
251+
screen.getByText("Agent name must start with a letter."),
252+
).toBeInTheDocument();
253+
expect(
254+
screen.queryByText("Agent name is available."),
255+
).not.toBeInTheDocument();
256+
});
257+
244258
it("does not show validation error for empty name", () => {
245259
render(<StepType />);
246260
expect(

src/frontend/src/pages/MainPage/pages/deploymentsPage/components/step-attach-flows-version-panel.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { memo } from "react";
22
import VersionLabel from "@/components/common/versionLabelComponent";
33
import { Badge } from "@/components/ui/badge";
4+
import { Button } from "@/components/ui/button";
45
import type { FlowType } from "@/types/flow";
56
import type { FlowVersionEntry } from "@/types/flow/version";
67
import { cn } from "@/utils/utils";
@@ -19,14 +20,18 @@ export const VersionPanel = memo(function VersionPanel({
1920
selectedFlow,
2021
versions,
2122
isLoadingVersions,
23+
isCreatingDraftVersion,
2224
selectedVersionByFlow,
2325
onAttach,
26+
onCreateFromDraft,
2427
}: {
2528
selectedFlow: FlowType | undefined;
2629
versions: FlowVersionEntry[];
2730
isLoadingVersions: boolean;
31+
isCreatingDraftVersion: boolean;
2832
selectedVersionByFlow: Map<string, { versionId: string; versionTag: string }>;
2933
onAttach: (versionId: string) => void;
34+
onCreateFromDraft: () => void;
3035
}) {
3136
if (!selectedFlow) {
3237
return (
@@ -53,8 +58,19 @@ export const VersionPanel = memo(function VersionPanel({
5358
)}
5459

5560
{!isLoadingVersions && versions.length === 0 && (
56-
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
57-
No versions found
61+
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-dashed border-border bg-muted/30 px-6 py-8 text-center">
62+
<p className="max-w-sm text-sm text-muted-foreground">
63+
Deploy this flow by creating a version from current Draft
64+
</p>
65+
<Button
66+
onClick={onCreateFromDraft}
67+
loading={isCreatingDraftVersion}
68+
disabled={isCreatingDraftVersion}
69+
ignoreTitleCase
70+
data-testid="create-version-from-draft"
71+
>
72+
Create from Draft
73+
</Button>
5874
</div>
5975
)}
6076

src/frontend/src/pages/MainPage/pages/deploymentsPage/components/step-attach-flows.tsx

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
22
import { useParams } from "react-router-dom";
33
import { useGetDeploymentConfigs } from "@/controllers/API/queries/deployments/use-get-deployment-configs";
44
import { useGetFlowVersions } from "@/controllers/API/queries/flow-version/use-get-flow-versions";
5+
import { usePostCreateSnapshot } from "@/controllers/API/queries/flow-version/use-post-create-snapshot";
56
import { useGetRefreshFlowsQuery } from "@/controllers/API/queries/flows/use-get-refresh-flows-query";
67
import { useGetGlobalVariables } from "@/controllers/API/queries/variables";
78
import { usePostDetectEnvVars } from "@/controllers/API/queries/variables/use-post-detect-env-vars";
@@ -25,7 +26,6 @@ export default function StepAttachFlows() {
2526
setConnections,
2627
selectedVersionByFlow,
2728
handleSelectVersion: onSelectVersion,
28-
toolNameByFlow,
2929
setToolNameByFlow,
3030
attachedConnectionByFlow,
3131
setAttachedConnectionByFlow: onAttachConnection,
@@ -157,13 +157,13 @@ export default function StepAttachFlows() {
157157
});
158158

159159
const { mutateAsync: detectEnvVars } = usePostDetectEnvVars();
160+
const { mutateAsync: createSnapshot, isPending: isCreatingDraftVersion } =
161+
usePostCreateSnapshot();
160162
const { data: globalVariables } = useGetGlobalVariables();
161163
const globalVariableOptions = (globalVariables ?? []).map((v) => v.name);
162164

163165
// When a flow+version are pre-selected from outside (e.g., canvas deploy button),
164166
// auto-advance to the connections panel and detect env vars for the pre-selected version.
165-
// biome-ignore lint/correctness/useExhaustiveDependencies: intentionally run only on mount
166-
// eslint-disable-next-line react-hooks/exhaustive-deps
167167
useEffect(() => {
168168
const preSelected = initialFlowId
169169
? selectedVersionByFlow.get(initialFlowId)
@@ -197,18 +197,16 @@ export default function StepAttachFlows() {
197197

198198
const selectedFlow = flows.find((f) => f.id === effectiveFlowId);
199199

200-
const handleAttachFlow = useCallback(
201-
async (versionId: string) => {
202-
if (!effectiveFlowId) return;
203-
const version = versions.find((v) => v.id === versionId);
200+
const openConnectionPanelForVersion = useCallback(
201+
async (flowId: string, versionId: string, versionTag: string) => {
204202
// Don't commit to context yet — wait for connection step to complete.
205203
setPendingAttachment({
206-
flowId: effectiveFlowId,
204+
flowId,
207205
versionId,
208-
versionTag: version?.version_tag ?? "",
206+
versionTag,
209207
});
210208
setRightPanel("connections");
211-
initConnectionsForFlow(effectiveFlowId);
209+
initConnectionsForFlow(flowId);
212210

213211
// Auto-detect global variable references via the backend detection endpoint
214212
try {
@@ -225,15 +223,51 @@ export default function StepAttachFlows() {
225223
}
226224
},
227225
[
228-
effectiveFlowId,
229-
versions,
230226
detectEnvVars,
231-
setErrorData,
232227
initConnectionsForFlow,
228+
setErrorData,
233229
updateDetectedEnvVars,
234230
],
235231
);
236232

233+
const handleAttachFlow = useCallback(
234+
async (versionId: string) => {
235+
if (!effectiveFlowId) return;
236+
const version = versions.find((v) => v.id === versionId);
237+
await openConnectionPanelForVersion(
238+
effectiveFlowId,
239+
versionId,
240+
version?.version_tag ?? "",
241+
);
242+
},
243+
[effectiveFlowId, versions, openConnectionPanelForVersion],
244+
);
245+
246+
const handleCreateVersionFromDraft = useCallback(async () => {
247+
if (!effectiveFlowId) return;
248+
249+
try {
250+
const snapshot = await createSnapshot({ flowId: effectiveFlowId });
251+
await openConnectionPanelForVersion(
252+
effectiveFlowId,
253+
snapshot.id,
254+
snapshot.version_tag,
255+
);
256+
} catch (err: unknown) {
257+
const detail = (err as { response?: { data?: { detail?: string } } })
258+
?.response?.data?.detail;
259+
setErrorData({
260+
title: "Failed to create version from draft",
261+
...(detail ? { list: [detail] } : {}),
262+
});
263+
}
264+
}, [
265+
createSnapshot,
266+
effectiveFlowId,
267+
openConnectionPanelForVersion,
268+
setErrorData,
269+
]);
270+
237271
const handleDetachFlow = useCallback(
238272
(flowId: string) => {
239273
handleRemoveAttachedFlow(flowId);
@@ -281,8 +315,10 @@ export default function StepAttachFlows() {
281315
selectedFlow={selectedFlow}
282316
versions={versions}
283317
isLoadingVersions={isLoadingVersions}
318+
isCreatingDraftVersion={isCreatingDraftVersion}
284319
selectedVersionByFlow={selectedVersionByFlow}
285320
onAttach={handleAttachFlow}
321+
onCreateFromDraft={handleCreateVersionFromDraft}
286322
/>
287323
) : (
288324
<ConnectionPanel

src/frontend/src/pages/MainPage/pages/deploymentsPage/components/step-type.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export default function StepType() {
123123
!isEditMode &&
124124
!!providerId &&
125125
!!trimmedDeploymentName &&
126+
!hasDeploymentNameFormatError &&
126127
!hasAgentNameErrors &&
127128
!isAgentNameValidationPending;
128129

0 commit comments

Comments
 (0)