diff --git a/backend/visitran/adapters/duckdb/seed.py b/backend/visitran/adapters/duckdb/seed.py index d68bcf9..7ae0fde 100644 --- a/backend/visitran/adapters/duckdb/seed.py +++ b/backend/visitran/adapters/duckdb/seed.py @@ -2,6 +2,7 @@ from visitran.adapters.duckdb.connection import DuckDbConnection from visitran.adapters.seed import BaseSeed +from visitran.errors import SeedFailureException class DuckDbSeed(BaseSeed): @@ -21,6 +22,20 @@ def execute(self) -> None: Overiding the base execute method to use the duckdb inbuild method """ + if self.db_connection is None: + raise SeedFailureException( + seed_file_name=self.csv_file_name, + error_message="Database connection is not available. Please check your connection settings.", + ) + # The drop SQL query will drop the table if it is only exists ! # Constructing SQL statement for CSV schema in target adapters - self.db_connection.insert_csv_records(abs_path=self.abs_path, table_name=self.destination_table_name) + try: + self.db_connection.insert_csv_records(abs_path=self.abs_path, table_name=self.destination_table_name) + except SeedFailureException: + raise + except Exception as err: + raise SeedFailureException( + seed_file_name=self.csv_file_name, + error_message=f'Failed to seed table "{self.destination_table_name}": {err}', + ) diff --git a/frontend/src/base/components/environment/NewEnv.jsx b/frontend/src/base/components/environment/NewEnv.jsx index a118013..9f83e08 100644 --- a/frontend/src/base/components/environment/NewEnv.jsx +++ b/frontend/src/base/components/environment/NewEnv.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useMemo } from "react"; +import { useState, useEffect, useCallback } from "react"; import Cookies from "js-cookie"; import { Button, Divider, Modal, Space } from "antd"; import PropTypes from "prop-types"; @@ -6,10 +6,8 @@ import isEqual from "lodash/isEqual.js"; import { useAxiosPrivate } from "../../../service/axios-service"; import { orgStore } from "../../../store/org-store"; -import { generateKey } from "../../../common/helpers"; import { fetchAllConnections, - fetchProjectByConnection, fetchSingleEnvironment, updateEnvironmentApi, createEnvironmentApi, @@ -23,7 +21,6 @@ import encryptionService from "../../../service/encryption-service"; import "./environment.css"; import EnvGeneralSection from "./EnvGeneralSection"; -import EnvCustomDataSection from "./EnvCustomDataSection"; import { CreateConnection } from "./CreateConnection"; import { useNotificationService } from "../../../service/notification-service"; @@ -41,7 +38,6 @@ const NewEnv = ({ const [isModalOpen, setIsModalOpen] = useState(false); const [connectionList, setConnectionList] = useState([]); - const [customData, setCustomData] = useState([]); const [connection, setConnection] = useState({ id: "" }); const [connectionDataSource, setConnectionDataSource] = useState(null); const [connectionSchema, setConnectionSchema] = useState({}); @@ -53,13 +49,11 @@ const NewEnv = ({ const [connType, setConnType] = useState("url"); const [connectionDetailsUpdated, setConnectionDetailsUpdated] = useState(false); - const [projListDep, setProjListDep] = useState([]); const [connectDetailBackup, setConnectDetailBackup] = useState({}); const [initialPrefillData, setInitialPrefillData] = useState({ name: "", description: "", deployment_type: "", - custom_data: [], }); const [activeUpdateBtn, setActiveUpdateBtn] = useState(false); const [isEncryptionLoading, setIsEncryptionLoading] = useState(true); @@ -109,7 +103,6 @@ const NewEnv = ({ description: "", deployment_type: "", }); - setCustomData([]); setInputFields({}); setConnection({ id: "" }); setConnectionDataSource(null); @@ -121,7 +114,6 @@ const NewEnv = ({ name: "", description: "", deployment_type: "", - custom_data: [], }); setConnectDetailBackup({}); setIsCredentialsRevealed(false); @@ -135,7 +127,6 @@ const NewEnv = ({ description: "", deployment_type: "", }); - setCustomData([]); setInputFields({}); setConnection({ id: "" }); setConnectionDataSource(null); @@ -147,7 +138,6 @@ const NewEnv = ({ name: "", description: "", deployment_type: "", - custom_data: [], }); setConnectDetailBackup({}); setIsCredentialsRevealed(false); @@ -278,16 +268,6 @@ const NewEnv = ({ [selectedOrgId] ); - const getProjectDependency = useCallback(async () => { - try { - const data = await fetchProjectByConnection(axiosRef, selectedOrgId, id); - setProjListDep(data); - } catch (error) { - console.error(error); - notify({ error }); - } - }, [id, selectedOrgId]); - useEffect(() => { getAllConnections(); }, [getAllConnections]); @@ -295,8 +275,7 @@ const NewEnv = ({ const getSingleEnvironmentDetails = useCallback(async () => { try { const data = await fetchSingleEnvironment(axiosRef, selectedOrgId, id); - const { connection, name, description, custom_data, deployment_type } = - data; + const { connection, name, description, deployment_type } = data; const connDetail = data.connection_details; setEnvNameDescInfo({ name, description, deployment_type }); @@ -304,14 +283,12 @@ const NewEnv = ({ name, description, deployment_type, - custom_data, }); setConnectDetailBackup({ connection_details: connDetail }); setConnType( connDetail?.connection_type ? connDetail?.connection_type : "host" ); setConnection({ id: connection.id }); - setCustomData(Array.isArray(custom_data) ? custom_data : []); // Process connection details to handle JSON objects for textarea fields const processedConnDetail = { ...connDetail }; @@ -338,14 +315,13 @@ const NewEnv = ({ useEffect(() => { if (id) { getSingleEnvironmentDetails(); - getProjectDependency(); } else { setConnection({ id: "" }); setEnvNameDescInfo({ name: "", description: "", deployment_type: "" }); setConnectionDataSource(null); setConnectionSchema({}); } - }, [id, getSingleEnvironmentDetails, getProjectDependency]); + }, [id, getSingleEnvironmentDetails]); const handleEnvNameDesChange = (value, name) => { setEnvNameDescInfo((prev) => ({ ...prev, [name]: value })); @@ -453,29 +429,6 @@ const NewEnv = ({ connectionDataSource, ]); - const AddnewEntry = () => { - const singleData = { source_schema: "", destination_schema: "" }; - setCustomData((prev) => [...prev, { id: generateKey(), ...singleData }]); - }; - - const handleCustomFieldChange = (value, name, rowId) => { - setCustomData((prev) => - prev.map((item) => - item.id === rowId ? { ...item, [name]: value } : item - ) - ); - }; - - const disabledAddCustomBtn = useMemo(() => { - return customData.some( - (item) => !item.source_schema || !item.destination_schema - ); - }, [customData]); - - const handleDelete = (rowId) => { - setCustomData((prev) => prev.filter((el) => el.id !== rowId)); - }; - const updateEnvironment = async () => { try { // Prepare environment data @@ -488,7 +441,6 @@ const NewEnv = ({ connection_type: connType, }), }, - custom_data: customData, }; // Encrypt sensitive fields if encryption service is available @@ -542,7 +494,6 @@ const NewEnv = ({ connection_type: connType, }), }, - custom_data: customData, }; // Encrypt sensitive fields if encryption service is available @@ -577,7 +528,6 @@ const NewEnv = ({ message: "Environment Created Successfully", }); setEnvNameDescInfo({ name: "", description: "", deployment_type: "" }); - setCustomData([]); setConnection({ id: "" }); } } catch (error) { @@ -597,10 +547,9 @@ const NewEnv = ({ name: envNameDescInfo.name, description: envNameDescInfo.description, deployment_type: envNameDescInfo.deployment_type, - custom_data: customData, }); setActiveUpdateBtn(!res); - }, [envNameDescInfo, customData, initialPrefillData]); + }, [envNameDescInfo, initialPrefillData]); useEffect(() => { const res = isEqual(connectDetailBackup, { @@ -641,19 +590,6 @@ const NewEnv = ({ - - - -
diff --git a/frontend/src/ide/editor/no-code-model/no-code-model.jsx b/frontend/src/ide/editor/no-code-model/no-code-model.jsx index 220a04a..8f3fa3f 100644 --- a/frontend/src/ide/editor/no-code-model/no-code-model.jsx +++ b/frontend/src/ide/editor/no-code-model/no-code-model.jsx @@ -92,6 +92,7 @@ import { OpenTab, } from "../../../base/icons/index.js"; import { SpinnerLoader } from "../../../widgets/spinner_loader/index.js"; +import { CopyableCell } from "../../../widgets/copyable-cell/index.js"; import "./no-code-model.css"; import "reactflow/dist/style.css"; import { useNotificationService } from "../../../service/notification-service.js"; @@ -743,6 +744,7 @@ function NoCodeModel({ nodeData }) { }) .catch((error) => { notify({ error }); + setIsLoading(false); handleModalClose(); }) .finally(() => { @@ -1362,23 +1364,33 @@ function NoCodeModel({ nodeData }) { ? JSON.stringify(text) : text; + const formattedValue = + displayValue === null || displayValue === undefined + ? "" + : ["Boolean", "boolean"].includes(dataType) + ? String(displayValue) + : dataType === "Number" + ? formatNumber(displayValue) + : displayValue; + + const alignClass = + dataType === "Number" + ? "flex-justify-right" + : ["Boolean", "boolean"].includes(dataType) + ? "flex-justify-center" + : ""; + return ( - - {["Boolean", "boolean"].includes(dataType) - ? String(displayValue) - : dataType === "Number" - ? formatNumber(displayValue) - : displayValue} - + {formattedValue} + ); }, }; diff --git a/frontend/src/service/notification-service.js b/frontend/src/service/notification-service.js index 7997e8a..5266ba7 100644 --- a/frontend/src/service/notification-service.js +++ b/frontend/src/service/notification-service.js @@ -60,7 +60,7 @@ function NotificationProvider({ children }) { if (errorData && typeof errorData.error_message === "string") { const text = errorData.error_message; if (errorData.is_markdown) { - finalMessage = ""; + finalMessage = "Failed"; const formatted = text.replace(/\n/g, " \n"); finalDescription = formatted; type = errorData?.severity?.toLowerCase() || "error"; diff --git a/frontend/src/widgets/copyable-cell/copyable-cell.css b/frontend/src/widgets/copyable-cell/copyable-cell.css new file mode 100644 index 0000000..2175468 --- /dev/null +++ b/frontend/src/widgets/copyable-cell/copyable-cell.css @@ -0,0 +1,26 @@ +/* Copyable Cell - hover tooltip with copy for table cells */ + +.copyable-cell-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: default; +} + +/* Tooltip content layout */ +.copyable-cell-tooltip { + display: flex; + align-items: flex-start; + gap: 8px; + font-family: var(--font-family); +} + +.copyable-cell-tooltip-text { + flex: 1; + min-width: 0; + word-break: break-word; + white-space: pre-wrap; + user-select: text; + font-size: 12px; + line-height: 1.5; +} diff --git a/frontend/src/widgets/copyable-cell/index.js b/frontend/src/widgets/copyable-cell/index.js new file mode 100644 index 0000000..9f1bfaf --- /dev/null +++ b/frontend/src/widgets/copyable-cell/index.js @@ -0,0 +1,124 @@ +import { memo, useState, useCallback, useRef, useEffect, useMemo } from "react"; +import PropTypes from "prop-types"; +import { Tooltip, theme } from "antd"; +import { CopyOutlined, CheckOutlined } from "@ant-design/icons"; + +import "./copyable-cell.css"; + +const { useToken } = theme; +const MAX_TOOLTIP_LENGTH = 500; + +const CopyableCell = memo(function CopyableCell({ + children, + value, + className = "", +}) { + const [copied, setCopied] = useState(false); + const timeoutRef = useRef(null); + const { token } = useToken(); + + const textValue = value !== undefined && value !== null ? String(value) : ""; + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []); + + const doCopy = useCallback( + async (e) => { + e.stopPropagation(); + if (!textValue) return; + try { + await navigator?.clipboard?.writeText(textValue); + setCopied(true); + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setCopied(false), 2000); + } catch { + // silently fail – clipboard may not be available + } + }, + [textValue] + ); + + const overlayStyle = useMemo( + () => ({ + border: `1px solid ${token.colorBorder}`, + borderRadius: "8px", + }), + [token.colorBorder] + ); + + const copiedIconStyle = useMemo( + () => ({ color: token.colorSuccess, fontSize: 14, padding: 2 }), + [token.colorSuccess] + ); + + const copyIconStyle = useMemo( + () => ({ + cursor: "pointer", + color: token.colorTextSecondary, + fontSize: 14, + padding: 2, + }), + [token.colorTextSecondary] + ); + + if (!textValue) { + return {children}; + } + + const truncatedValue = + textValue.length > MAX_TOOLTIP_LENGTH + ? textValue.slice(0, MAX_TOOLTIP_LENGTH) + "..." + : textValue; + + const tooltipContent = ( +
+ + {truncatedValue} + + {copied ? ( + + ) : ( + + )} +
+ ); + + return ( + + + + {children} + + + + ); +}); + +CopyableCell.propTypes = { + children: PropTypes.node, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool, + ]), + className: PropTypes.string, +}; + +export { CopyableCell };