diff --git a/packages/bruno-app/src/components/EditableTable/StyledWrapper.js b/packages/bruno-app/src/components/EditableTable/StyledWrapper.js index f980560ed..4da89b669 100644 --- a/packages/bruno-app/src/components/EditableTable/StyledWrapper.js +++ b/packages/bruno-app/src/components/EditableTable/StyledWrapper.js @@ -6,8 +6,13 @@ const StyledWrapper = styled.div` flex: 1; overflow: hidden; + &.is-resizing { + cursor: col-resize !important; + user-select: none; + } + .table-container { - overflow-y: auto; + overflow: auto; border-radius: ${(props) => props.theme.border.radius.base}; border: solid 1px ${(props) => props.theme.border.border0}; } @@ -24,6 +29,7 @@ const StyledWrapper = styled.div` color: ${(props) => props.theme.table.thead.color} !important; background: ${(props) => props.theme.sidebar.bg}; user-select: none; + overflow: visible; border: none !important; @@ -34,10 +40,36 @@ const StyledWrapper = styled.div` border-bottom: solid 1px ${(props) => props.theme.border.border0}; border-right: solid 1px ${(props) => props.theme.border.border0}; vertical-align: middle; + position: relative; + overflow: visible; &:last-child { border-right: none; } + + .column-name { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-right: 4px; + } + + .resize-handle { + position: absolute; + right: 0; + top: 0; + width: 4px; + height: 100%; + cursor: col-resize; + background: transparent; + z-index: 100; + + &:hover, + &.resizing { + background: ${(props) => props.theme.colors.accent}; + } + } } } @@ -61,10 +93,32 @@ const StyledWrapper = styled.div` border-bottom: solid 1px ${(props) => props.theme.border.border0}; border-right: solid 1px ${(props) => props.theme.border.border0}; vertical-align: middle; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; &:last-child { border-right: none; } + + /* Handle CodeMirror editors overflow */ + .cm-editor { + max-width: 100%; + + .cm-scroller { + overflow: hidden !important; + } + + .cm-content { + max-width: 100%; + } + + .cm-line { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } } } } diff --git a/packages/bruno-app/src/components/EditableTable/index.js b/packages/bruno-app/src/components/EditableTable/index.js index 6f2603624..64f0e5132 100644 --- a/packages/bruno-app/src/components/EditableTable/index.js +++ b/packages/bruno-app/src/components/EditableTable/index.js @@ -1,9 +1,11 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { IconTrash, IconAlertCircle, IconGripVertical, IconMinusVertical } from '@tabler/icons'; import { Tooltip } from 'react-tooltip'; import { uuid } from 'utils/common'; import StyledWrapper from './StyledWrapper'; +const MIN_COLUMN_WIDTH = 80; + const EditableTable = ({ columns, rows, @@ -23,7 +25,101 @@ const EditableTable = ({ const tableRef = useRef(null); const emptyRowUidRef = useRef(null); const [hoveredRow, setHoveredRow] = useState(null); - const [dragStart, setDragStart] = useState(null); + const [resizing, setResizing] = useState(null); + const [tableHeight, setTableHeight] = useState(0); + const [columnWidths, setColumnWidths] = useState(() => { + const initialWidths = {}; + columns.forEach((col) => { + initialWidths[col.key] = col.width || 'auto'; + }); + return initialWidths; + }); + + const handleResizeStart = useCallback((e, columnKey) => { + e.preventDefault(); + e.stopPropagation(); + + const currentCell = e.target.closest('td'); + const nextCell = currentCell?.nextElementSibling; + if (!currentCell || !nextCell) return; + + const columnIndex = columns.findIndex((col) => col.key === columnKey); + if (columnIndex >= columns.length - 1) return; + + const startX = e.clientX; + const startWidth = currentCell.offsetWidth; + const nextColumnKey = columns[columnIndex + 1].key; + const nextColumnStartWidth = nextCell.offsetWidth; + + setResizing(columnKey); + + const handleMouseMove = (moveEvent) => { + const diff = moveEvent.clientX - startX; + const maxGrow = nextColumnStartWidth - MIN_COLUMN_WIDTH; + const maxShrink = startWidth - MIN_COLUMN_WIDTH; + const clampedDiff = Math.max(-maxShrink, Math.min(maxGrow, diff)); + + setColumnWidths((prev) => ({ + ...prev, + [columnKey]: `${startWidth + clampedDiff}px`, + [nextColumnKey]: `${nextColumnStartWidth - clampedDiff}px` + })); + }; + + const handleMouseUp = () => { + // Convert pixel widths to percentages for responsive scaling + const table = tableRef.current?.querySelector('table'); + if (table) { + const tableWidth = table.offsetWidth; + const headerCells = table.querySelectorAll('thead td'); + const newWidths = {}; + + headerCells.forEach((cell, cellIndex) => { + const checkboxOffset = showCheckbox ? 1 : 0; + const colIndex = cellIndex - checkboxOffset; + + if (colIndex >= 0 && colIndex < columns.length) { + const colKey = columns[colIndex]?.key; + if (colKey) { + const percentage = (cell.offsetWidth / tableWidth) * 100; + newWidths[colKey] = `${percentage}%`; + } + } + }); + + if (Object.keys(newWidths).length > 0) { + setColumnWidths((prev) => ({ ...prev, ...newWidths })); + } + } + setResizing(null); + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }, [columns, showCheckbox]); + + // Track table height for resize handles + useEffect(() => { + const table = tableRef.current?.querySelector('table'); + if (!table) return; + + const updateHeight = () => { + setTableHeight(table.offsetHeight); + }; + + updateHeight(); + + const resizeObserver = new ResizeObserver(updateHeight); + resizeObserver.observe(table); + + return () => resizeObserver.disconnect(); + }, [rows.length]); + + const getColumnWidth = useCallback((column) => { + return columnWidths[column.key] || column.width || 'auto'; + }, [columnWidths]); const createEmptyRow = useCallback(() => { const newUid = uuid(); @@ -138,15 +234,9 @@ const EditableTable = ({ onChange(filteredRows); }, [rows, onChange]); - const getColumnWidth = useCallback((column) => { - if (column.width) return column.width; - return 'auto'; - }, []); - const handleDragStart = useCallback((e, index) => { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', index); - setDragStart(index); }, []); const handleDragOver = useCallback((e, index) => { @@ -163,19 +253,16 @@ const EditableTable = ({ const updatedOrder = [...reorderableRows]; const [movedRow] = updatedOrder.splice(fromIndex, 1); if (!movedRow) { - setDragStart(null); setHoveredRow(null); return; } updatedOrder.splice(toIndex, 0, movedRow); onReorder({ updateReorderedItem: updatedOrder.map((row) => row.uid) }); } - setDragStart(null); setHoveredRow(null); }, [onReorder, rowsWithEmpty, showAddRow]); const handleDragEnd = useCallback(() => { - setDragStart(null); setHoveredRow(null); }, []); @@ -236,7 +323,7 @@ const EditableTable = ({ const reorderableRowCount = showAddRow ? rowsWithEmpty.length - 1 : rowsWithEmpty.length; return ( - +
@@ -244,12 +331,19 @@ const EditableTable = ({ {showCheckbox && ( )} - {columns.map((column) => ( + {columns.map((column, colIndex) => ( ))} {showDelete && ( diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/StyledWrapper.js b/packages/bruno-app/src/components/EnvironmentVariablesTable/StyledWrapper.js similarity index 86% rename from packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/StyledWrapper.js rename to packages/bruno-app/src/components/EnvironmentVariablesTable/StyledWrapper.js index 0f378dc01..dafdae4d8 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/StyledWrapper.js +++ b/packages/bruno-app/src/components/EnvironmentVariablesTable/StyledWrapper.js @@ -6,6 +6,11 @@ const Wrapper = styled.div` flex: 1; overflow: hidden; + &.is-resizing { + cursor: col-resize !important; + user-select: none; + } + .table-container { overflow-y: auto; border-radius: 8px; @@ -32,10 +37,6 @@ const Wrapper = styled.div` &:nth-child(5) { width: 60px; } - - &:nth-child(2) { - width: 30%; - } } thead { @@ -48,10 +49,26 @@ const Wrapper = styled.div` padding: 5px 10px !important; border-bottom: solid 1px ${(props) => props.theme.border.border0}; border-right: solid 1px ${(props) => props.theme.border.border0}; + position: relative; &:last-child { border-right: none; } + + .resize-handle { + position: absolute; + right: 0; + top: 0; + width: 4px; + cursor: col-resize; + background: transparent; + z-index: 100; + + &:hover, + &.resizing { + background: ${(props) => props.theme.colors.accent}; + } + } } } @@ -147,21 +164,6 @@ const Wrapper = styled.div` opacity: 0.9; } } - - .discard { - padding: 6px 16px; - font-size: ${(props) => props.theme.font.size.sm}; - border-radius: ${(props) => props.theme.border.radius.base}; - background: transparent; - color: ${(props) => props.theme.text}; - border: 1px solid ${(props) => props.theme.border.border1}; - cursor: pointer; - transition: all 0.15s ease; - - &:hover { - background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; - } - } `; export default Wrapper; diff --git a/packages/bruno-app/src/components/EnvironmentVariablesTable/index.js b/packages/bruno-app/src/components/EnvironmentVariablesTable/index.js new file mode 100644 index 000000000..8ae4e801d --- /dev/null +++ b/packages/bruno-app/src/components/EnvironmentVariablesTable/index.js @@ -0,0 +1,546 @@ +import React, { useCallback, useRef, useState, useEffect, useMemo } from 'react'; +import { TableVirtuoso } from 'react-virtuoso'; +import cloneDeep from 'lodash/cloneDeep'; +import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons'; +import { useTheme } from 'providers/Theme'; +import { useSelector } from 'react-redux'; +import MultiLineEditor from 'components/MultiLineEditor/index'; +import StyledWrapper from './StyledWrapper'; +import { uuid } from 'utils/common'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; +import { variableNameRegex } from 'utils/common/regex'; +import toast from 'react-hot-toast'; +import { Tooltip } from 'react-tooltip'; +import { getGlobalEnvironmentVariables } from 'utils/collections'; + +const MIN_H = 35 * 2; +const MIN_COLUMN_WIDTH = 80; + +const TableRow = React.memo( + ({ children, item }) => ( + + {children} + + ), + (prevProps, nextProps) => { + const prevUid = prevProps?.item?.uid; + const nextUid = nextProps?.item?.uid; + return prevUid === nextUid && prevProps.children === nextProps.children; + } +); + +const EnvironmentVariablesTable = ({ + environment, + collection, + onSave, + draft, + onDraftChange, + onDraftClear, + setIsModified, + renderExtraValueContent, + searchQuery = '' +}) => { + const { storedTheme } = useTheme(); + const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments); + + const hasDraftForThisEnv = draft?.environmentUid === environment.uid; + + const [tableHeight, setTableHeight] = useState(MIN_H); + const [columnWidths, setColumnWidths] = useState({ name: '30%', value: 'auto' }); + const [resizing, setResizing] = useState(null); + + const handleResizeStart = useCallback((e, columnKey) => { + e.preventDefault(); + e.stopPropagation(); + + const currentCell = e.target.closest('td'); + const nextCell = currentCell?.nextElementSibling; + if (!currentCell || !nextCell) return; + + const startX = e.clientX; + const startWidth = currentCell.offsetWidth; + const nextColumnKey = 'value'; + const nextColumnStartWidth = nextCell.offsetWidth; + + setResizing(columnKey); + + const handleMouseMove = (moveEvent) => { + const diff = moveEvent.clientX - startX; + const maxGrow = nextColumnStartWidth - MIN_COLUMN_WIDTH; + const maxShrink = startWidth - MIN_COLUMN_WIDTH; + const clampedDiff = Math.max(-maxShrink, Math.min(maxGrow, diff)); + + setColumnWidths({ + [columnKey]: `${startWidth + clampedDiff}px`, + [nextColumnKey]: `${nextColumnStartWidth - clampedDiff}px` + }); + }; + + const handleMouseUp = () => { + setResizing(null); + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }, []); + + const handleTotalHeightChanged = useCallback((h) => { + setTableHeight(h); + }, []); + + const prevEnvUidRef = useRef(null); + const mountedRef = useRef(false); + + let _collection = collection ? cloneDeep(collection) : {}; + const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid }); + if (_collection) { + _collection.globalEnvironmentVariables = globalEnvironmentVariables; + } + + const initialValues = useMemo(() => { + const vars = environment.variables || []; + return [ + ...vars, + { + uid: uuid(), + name: '', + value: '', + type: 'text', + secret: false, + enabled: true + } + ]; + }, [environment.uid, environment.variables]); + + const formik = useFormik({ + enableReinitialize: true, + initialValues: initialValues, + validationSchema: Yup.array().of( + Yup.object({ + enabled: Yup.boolean(), + name: Yup.string().when('$isLastRow', { + is: true, + then: (schema) => schema.optional(), + otherwise: (schema) => + schema + .required('Name cannot be empty') + .matches( + variableNameRegex, + 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.' + ) + .trim() + }), + secret: Yup.boolean(), + type: Yup.string(), + uid: Yup.string(), + value: Yup.mixed().nullable() + }) + ), + validate: (values) => { + const errors = {}; + values.forEach((variable, index) => { + const isLastRow = index === values.length - 1; + const isEmptyRow = !variable.name || variable.name.trim() === ''; + + if (isLastRow && isEmptyRow) { + return; + } + + if (!variable.name || variable.name.trim() === '') { + if (!errors[index]) errors[index] = {}; + errors[index].name = 'Name cannot be empty'; + } else if (!variableNameRegex.test(variable.name)) { + if (!errors[index]) errors[index] = {}; + errors[index].name + = 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.'; + } + }); + return Object.keys(errors).length > 0 ? errors : {}; + }, + onSubmit: () => {} + }); + + // Restore draft values on mount or environment switch + useEffect(() => { + const isMount = !mountedRef.current; + const envChanged = prevEnvUidRef.current !== null && prevEnvUidRef.current !== environment.uid; + + prevEnvUidRef.current = environment.uid; + mountedRef.current = true; + + if ((isMount || envChanged) && hasDraftForThisEnv && draft?.variables) { + formik.setValues([ + ...draft.variables, + { + uid: uuid(), + name: '', + value: '', + type: 'text', + secret: false, + enabled: true + } + ]); + } + }, [environment.uid, hasDraftForThisEnv, draft?.variables]); + + const savedValuesJson = useMemo(() => { + return JSON.stringify(environment.variables || []); + }, [environment.variables]); + + // Sync modified state + useEffect(() => { + const currentValues = formik.values.filter((variable) => variable.name && variable.name.trim() !== ''); + const currentValuesJson = JSON.stringify(currentValues); + const hasActualChanges = currentValuesJson !== savedValuesJson; + setIsModified(hasActualChanges); + }, [formik.values, savedValuesJson, setIsModified]); + + // Sync draft state + useEffect(() => { + const timeoutId = setTimeout(() => { + const currentValues = formik.values.filter((variable) => variable.name && variable.name.trim() !== ''); + const currentValuesJson = JSON.stringify(currentValues); + const hasActualChanges = currentValuesJson !== savedValuesJson; + + const existingDraftVariables = hasDraftForThisEnv ? draft?.variables : null; + const existingDraftJson = existingDraftVariables ? JSON.stringify(existingDraftVariables) : null; + + if (hasActualChanges) { + if (currentValuesJson !== existingDraftJson) { + onDraftChange(currentValues); + } + } else if (hasDraftForThisEnv) { + onDraftClear(); + } + }, 300); + + return () => clearTimeout(timeoutId); + }, [formik.values, savedValuesJson, environment.uid, hasDraftForThisEnv, draft?.variables, onDraftChange, onDraftClear]); + + const ErrorMessage = ({ name, index }) => { + const meta = formik.getFieldMeta(name); + const id = `error-${name}-${index}`; + + const isLastRow = index === formik.values.length - 1; + const variable = formik.values[index]; + const isEmptyRow = !variable?.name || variable.name.trim() === ''; + + if (isLastRow && isEmptyRow) { + return null; + } + + if (!meta.error || !meta.touched) { + return null; + } + return ( + + + + + ); + }; + + const handleRemoveVar = useCallback( + (id) => { + const currentValues = formik.values; + + if (!currentValues || currentValues.length === 0) { + return; + } + + const lastRow = currentValues[currentValues.length - 1]; + const isLastEmptyRow = lastRow?.uid === id && (!lastRow.name || lastRow.name.trim() === ''); + + if (isLastEmptyRow) { + return; + } + + const filteredValues = currentValues.filter((variable) => variable.uid !== id); + + const hasEmptyLastRow + = filteredValues.length > 0 + && (!filteredValues[filteredValues.length - 1].name + || filteredValues[filteredValues.length - 1].name.trim() === ''); + + const newValues = hasEmptyLastRow + ? filteredValues + : [ + ...filteredValues, + { + uid: uuid(), + name: '', + value: '', + type: 'text', + secret: false, + enabled: true + } + ]; + + formik.setValues(newValues); + }, + [formik.values] + ); + + const handleNameChange = (index, e) => { + formik.handleChange(e); + const isLastRow = index === formik.values.length - 1; + + if (isLastRow) { + const newVariable = { + uid: uuid(), + name: '', + value: '', + type: 'text', + secret: false, + enabled: true + }; + setTimeout(() => { + formik.setFieldValue(formik.values.length, newVariable, false); + }, 0); + } + }; + + const handleNameBlur = (index) => { + formik.setFieldTouched(`${index}.name`, true, true); + }; + + const handleNameKeyDown = (index, e) => { + if (e.key === 'Enter') { + e.preventDefault(); + formik.setFieldTouched(`${index}.name`, true, true); + } + }; + + const handleSave = useCallback(() => { + const variablesToSave = formik.values.filter((variable) => variable.name && variable.name.trim() !== ''); + const savedValues = environment.variables || []; + + const hasChanges = JSON.stringify(variablesToSave) !== JSON.stringify(savedValues); + if (!hasChanges) { + toast.error('No changes to save'); + return; + } + + const hasValidationErrors = variablesToSave.some((variable) => { + if (!variable.name || variable.name.trim() === '') { + return true; + } + if (!variableNameRegex.test(variable.name)) { + return true; + } + return false; + }); + + if (hasValidationErrors) { + toast.error('Please fix validation errors before saving'); + return; + } + + onSave(cloneDeep(variablesToSave)) + .then(() => { + toast.success('Changes saved successfully'); + const newValues = [ + ...variablesToSave, + { + uid: uuid(), + name: '', + value: '', + type: 'text', + secret: false, + enabled: true + } + ]; + formik.resetForm({ values: newValues }); + setIsModified(false); + }) + .catch((error) => { + console.error(error); + toast.error('An error occurred while saving the changes'); + }); + }, [formik.values, environment.variables, onSave, setIsModified]); + + const handleReset = useCallback(() => { + const originalVars = environment.variables || []; + const resetValues = [ + ...originalVars, + { + uid: uuid(), + name: '', + value: '', + type: 'text', + secret: false, + enabled: true + } + ]; + formik.resetForm({ values: resetValues }); + setIsModified(false); + }, [environment.variables, setIsModified]); + + const handleSaveRef = useRef(handleSave); + handleSaveRef.current = handleSave; + + useEffect(() => { + const handleSaveEvent = () => { + handleSaveRef.current(); + }; + + window.addEventListener('environment-save', handleSaveEvent); + + return () => { + window.removeEventListener('environment-save', handleSaveEvent); + }; + }, []); + + const filteredVariables = useMemo(() => { + const allVariables = formik.values.map((variable, index) => ({ variable, index })); + if (!searchQuery?.trim()) { + return allVariables; + } + + const query = searchQuery.toLowerCase().trim(); + + return allVariables.filter(({ variable, index }) => { + const isLastRow = index === formik.values.length - 1; + const isEmptyRow = !variable.name || variable.name.trim() === ''; + if (isLastRow && isEmptyRow) { + return true; + } + + const nameMatch = variable.name ? variable.name.toLowerCase().includes(query) : false; + const valueMatch = typeof variable.value === 'string' ? variable.value.toLowerCase().includes(query) : false; + + return !!(nameMatch || valueMatch); + }); + }, [formik.values, searchQuery]); + + return ( + + ( + + + + + + + + )} + fixedItemHeight={35} + computeItemKey={(index, item) => item.variable.uid} + itemContent={(index, { variable, index: actualIndex }) => { + const isLastRow = actualIndex === formik.values.length - 1; + const isEmptyRow = !variable.name || variable.name.trim() === ''; + const isLastEmptyRow = isLastRow && isEmptyRow; + + return ( + <> + + + + + + + ); + }} + /> + +
+
+ + +
+
+ + ); +}; + +export default EnvironmentVariablesTable; diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index a83ff0b34..266c62681 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -1,57 +1,20 @@ -import React, { useCallback, useRef, useMemo, useEffect } from 'react'; -import { TableVirtuoso } from 'react-virtuoso'; +import React, { useMemo, useCallback } from 'react'; import cloneDeep from 'lodash/cloneDeep'; import { get } from 'lodash'; -import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons'; -import { useTheme } from 'providers/Theme'; -import { useDispatch, useSelector } from 'react-redux'; -import MultiLineEditor from 'components/MultiLineEditor/index'; -import StyledWrapper from './StyledWrapper'; -import { uuid } from 'utils/common'; -import { useFormik } from 'formik'; -import * as Yup from 'yup'; -import { variableNameRegex } from 'utils/common/regex'; -import toast from 'react-hot-toast'; +import { useDispatch } from 'react-redux'; import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import { setEnvironmentsDraft, clearEnvironmentsDraft } from 'providers/ReduxStore/slices/collections'; -import { Tooltip } from 'react-tooltip'; -import { getGlobalEnvironmentVariables, flattenItems, isItemARequest } from 'utils/collections'; +import { flattenItems, isItemARequest } from 'utils/collections'; import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import EnvironmentVariablesTable from 'components/EnvironmentVariablesTable'; import { sensitiveFields } from './constants'; -const TableRow = React.memo(({ children, item }) => {children}, (prevProps, nextProps) => { - const prevUid = prevProps?.item?.variable?.uid; - const nextUid = nextProps?.item?.variable?.uid; - return prevUid === nextUid && prevProps.children === nextProps.children; -}); - -const MIN_H = 35 * 2; // 2 rows worth of height - const EnvironmentVariables = ({ environment, setIsModified, collection, searchQuery = '' }) => { const dispatch = useDispatch(); - const { storedTheme } = useTheme(); - const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments); - - const [tableHeight, setTableHeight] = React.useState(MIN_H); const environmentsDraft = collection?.environmentsDraft; const hasDraftForThisEnv = environmentsDraft?.environmentUid === environment.uid; - const handleTotalHeightChanged = React.useCallback((h) => { - setTableHeight(h); - }, []); - - // Track environment changes for draft restoration - const prevEnvUidRef = React.useRef(null); - const mountedRef = React.useRef(false); - - let _collection = collection ? cloneDeep(collection) : {}; - - const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid }); - if (_collection) { - _collection.globalEnvironmentVariables = globalEnvironmentVariables; - } - // Check for non-secret variables used in sensitive fields const nonSecretSensitiveVarUsageMap = useMemo(() => { const result = {}; @@ -97,450 +60,59 @@ const EnvironmentVariables = ({ environment, setIsModified, collection, searchQu return result; }, [collection, environment]); - const hasSensitiveUsage = (name) => !!nonSecretSensitiveVarUsageMap[name]; + const hasSensitiveUsage = useCallback((name) => !!nonSecretSensitiveVarUsageMap[name], [nonSecretSensitiveVarUsageMap]); - // Initial values based only on saved environment variables (not draft) - // Draft restoration happens in a separate effect to avoid infinite loops - const initialValues = React.useMemo(() => { - const vars = environment.variables || []; - return [ - ...vars, - { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - } - ]; - }, [environment.uid, environment.variables]); - - const formik = useFormik({ - enableReinitialize: true, - initialValues: initialValues, - validationSchema: Yup.array().of( - Yup.object({ - enabled: Yup.boolean(), - name: Yup.string() - .when('$isLastRow', { - is: true, - then: (schema) => schema.optional(), - otherwise: (schema) => - schema - .required('Name cannot be empty') - .matches( - variableNameRegex, - 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.' - ) - .trim() - }), - secret: Yup.boolean(), - type: Yup.string(), - uid: Yup.string(), - value: Yup.mixed().nullable() - }) - ), - validate: (values) => { - const errors = {}; - values.forEach((variable, index) => { - const isLastRow = index === values.length - 1; - const isEmptyRow = !variable.name || variable.name.trim() === ''; - - if (isLastRow && isEmptyRow) { - return; - } - - if (!variable.name || variable.name.trim() === '') { - if (!errors[index]) errors[index] = {}; - errors[index].name = 'Name cannot be empty'; - } else if (!variableNameRegex.test(variable.name)) { - if (!errors[index]) errors[index] = {}; - errors[index].name - = 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.'; - } - }); - return Object.keys(errors).length > 0 ? errors : {}; + const handleSave = useCallback( + (variables) => { + return dispatch(saveEnvironment(cloneDeep(variables), environment.uid, collection.uid)); }, - onSubmit: () => {} - }); + [dispatch, environment.uid, collection.uid] + ); - // Restore draft values on mount or environment switch - useEffect(() => { - const isMount = !mountedRef.current; - const envChanged = prevEnvUidRef.current !== null && prevEnvUidRef.current !== environment.uid; + const handleDraftChange = useCallback( + (variables) => { + dispatch( + setEnvironmentsDraft({ + collectionUid: collection.uid, + environmentUid: environment.uid, + variables + }) + ); + }, + [dispatch, collection.uid, environment.uid] + ); - prevEnvUidRef.current = environment.uid; - mountedRef.current = true; + const handleDraftClear = useCallback(() => { + dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid })); + }, [dispatch, collection.uid]); - if ((isMount || envChanged) && hasDraftForThisEnv && environmentsDraft?.variables) { - formik.setValues([ - ...environmentsDraft.variables, - { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - } - ]); - } - }, [environment.uid, hasDraftForThisEnv, environmentsDraft?.variables]); - - const savedValuesJson = useMemo(() => { - return JSON.stringify(environment.variables || []); - }, [environment.variables]); - - useEffect(() => { - const currentValues = formik.values.filter((variable) => variable.name && variable.name.trim() !== ''); - const currentValuesJson = JSON.stringify(currentValues); - const hasActualChanges = currentValuesJson !== savedValuesJson; - setIsModified(hasActualChanges); - }, [formik.values, savedValuesJson, setIsModified]); - - useEffect(() => { - const timeoutId = setTimeout(() => { - const currentValues = formik.values.filter((variable) => variable.name && variable.name.trim() !== ''); - const currentValuesJson = JSON.stringify(currentValues); - const hasActualChanges = currentValuesJson !== savedValuesJson; - - // Get existing draft for comparison - const existingDraftVariables = hasDraftForThisEnv ? environmentsDraft?.variables : null; - const existingDraftJson = existingDraftVariables ? JSON.stringify(existingDraftVariables) : null; - - if (hasActualChanges) { - // Only dispatch if draft values are actually different - if (currentValuesJson !== existingDraftJson) { - dispatch(setEnvironmentsDraft({ - collectionUid: collection.uid, - environmentUid: environment.uid, - variables: currentValues - })); - } - } else if (hasDraftForThisEnv) { - dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid })); + const renderExtraValueContent = useCallback( + (variable) => { + if (!variable.secret && hasSensitiveUsage(variable.name)) { + return ( + + ); } - }, 300); - - return () => clearTimeout(timeoutId); - }, [formik.values, savedValuesJson, environment.uid, collection.uid, dispatch, hasDraftForThisEnv, environmentsDraft?.variables]); - - const ErrorMessage = ({ name, index }) => { - const meta = formik.getFieldMeta(name); - const id = `error-${name}-${index}`; - - const isLastRow = index === formik.values.length - 1; - const variable = formik.values[index]; - const isEmptyRow = !variable?.name || variable.name.trim() === ''; - - if (isLastRow && isEmptyRow) { return null; - } - - if (!meta.error || !meta.touched) { - return null; - } - return ( - - - - - ); - }; - - const handleRemoveVar = useCallback((id) => { - const currentValues = formik.values; - - if (!currentValues || currentValues.length === 0) { - return; - } - - const lastRow = currentValues[currentValues.length - 1]; - const isLastEmptyRow = lastRow?.uid === id && (!lastRow.name || lastRow.name.trim() === ''); - - if (isLastEmptyRow) { - return; - } - - const filteredValues = currentValues.filter((variable) => variable.uid !== id); - - const hasEmptyLastRow - = filteredValues.length > 0 - && (!filteredValues[filteredValues.length - 1].name - || filteredValues[filteredValues.length - 1].name.trim() === ''); - - const newValues = hasEmptyLastRow - ? filteredValues - : [ - ...filteredValues, - { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - } - ]; - - formik.setValues(newValues); - }, [formik.values]); - - const handleNameChange = (index, e) => { - formik.handleChange(e); - const isLastRow = index === formik.values.length - 1; - - if (isLastRow) { - const newVariable = { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - }; - setTimeout(() => { - formik.setFieldValue(formik.values.length, newVariable, false); - }, 0); - } - }; - - const handleNameBlur = (index) => { - formik.setFieldTouched(`${index}.name`, true, true); - }; - - const handleNameKeyDown = (index, e) => { - if (e.key === 'Enter') { - e.preventDefault(); - formik.setFieldTouched(`${index}.name`, true, true); - } - }; - - const handleSave = () => { - const variablesToSave = formik.values.filter((variable) => variable.name && variable.name.trim() !== ''); - const savedValues = environment.variables || []; - - const hasChanges = JSON.stringify(variablesToSave) !== JSON.stringify(savedValues); - if (!hasChanges) { - toast.error('No changes to save'); - return; - } - - const hasValidationErrors = variablesToSave.some((variable) => { - if (!variable.name || variable.name.trim() === '') { - return true; - } - if (!variableNameRegex.test(variable.name)) { - return true; - } - return false; - }); - - if (hasValidationErrors) { - toast.error('Please fix validation errors before saving'); - return; - } - - dispatch(saveEnvironment(cloneDeep(variablesToSave), environment.uid, collection.uid)) - .then(() => { - toast.success('Changes saved successfully'); - const newValues = [ - ...variablesToSave, - { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - } - ]; - formik.resetForm({ values: newValues }); - setIsModified(false); - }) - .catch((error) => { - console.error(error); - toast.error('An error occurred while saving the changes'); - }); - }; - - const handleReset = () => { - const originalVars = environment.variables || []; - const resetValues = [ - ...originalVars, - { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - } - ]; - formik.resetForm({ values: resetValues }); - setIsModified(false); - }; - - const handleSaveRef = useRef(handleSave); - handleSaveRef.current = handleSave; - - useEffect(() => { - const handleSaveEvent = () => { - handleSaveRef.current(); - }; - - window.addEventListener('environment-save', handleSaveEvent); - - return () => { - window.removeEventListener('environment-save', handleSaveEvent); - }; - }, []); - - const filteredVariables = useMemo(() => { - const allVariables = formik.values.map((variable, index) => ({ variable, index })); - if (!searchQuery?.trim()) { - return allVariables; - } - - const query = searchQuery.toLowerCase().trim(); - - return allVariables.filter(({ variable, index }) => { - const isLastRow = index === formik.values.length - 1; - const isEmptyRow = !variable.name || variable.name.trim() === ''; - if (isLastRow && isEmptyRow) { - return true; - } - - const nameMatch = variable.name ? variable.name.toLowerCase().includes(query) : false; - const valueMatch = typeof variable.value === 'string' ? variable.value.toLowerCase().includes(query) : false; - - return !!(nameMatch || valueMatch); - }); - }, [formik.values, searchQuery]); + }, + [hasSensitiveUsage] + ); return ( - - ( - - - - - - - - )} - fixedItemHeight={35} - computeItemKey={(index, item) => item.variable.uid} - itemContent={(index, { variable, index: actualIndex }) => { - const isLastRow = actualIndex === formik.values.length - 1; - const isEmptyRow = !variable.name || variable.name.trim() === ''; - const isLastEmptyRow = isLastRow && isEmptyRow; - - return ( - <> - - - - - - - ); - }} - /> - -
-
- - -
-
- + ); }; diff --git a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js index e0eb81ee5..e5ce53337 100644 --- a/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js @@ -177,7 +177,7 @@ const MultipartFormParams = ({ item, collection }) => { key: 'contentType', name: 'Content-Type', placeholder: 'Auto', - width: '20%', + width: '30%', render: ({ row, value, onChange, isLastEmptyRow }) => ( props.theme.border.border0}; - } - - table { - width: 100%; - border-collapse: collapse; - table-layout: fixed; - - td { - vertical-align: middle; - padding: 2px 10px; - - &:nth-child(1) { - width: 25px; - border-right: none; - } - &:nth-child(4) { - width: 80px; - } - &:nth-child(5) { - width: 60px; - } - - &:nth-child(2) { - width: 30%; - } - } - - thead { - color: ${(props) => props.theme.table.thead.color} !important; - background: ${(props) => props.theme.sidebar.bg}; - font-size: ${(props) => props.theme.font.size.base}; - user-select: none; - - td { - padding: 5px 10px !important; - border-bottom: solid 1px ${(props) => props.theme.border.border0}; - border-right: solid 1px ${(props) => props.theme.border.border0}; - - &:last-child { - border-right: none; - } - } - } - - tbody { - tr { - transition: background 0.1s ease; - - &:last-child td { - border-bottom: none; - } - - td { - border-bottom: solid 1px ${(props) => props.theme.border.border0}; - border-right: solid 1px ${(props) => props.theme.border.border0}; - - &:last-child { - border-right: none; - } - } - } - } - } - - .tooltip-mod { - max-width: 200px !important; - } - - input[type='text'] { - width: 100%; - border: 1px solid transparent; - outline: none !important; - background-color: transparent; - color: ${(props) => props.theme.text}; - padding: 0; - border-radius: 4px; - transition: all 0.15s ease; - - &:focus { - outline: none !important; - } - } - - input[type='checkbox'] { - cursor: pointer; - width: 14px; - height: 14px; - accent-color: ${(props) => props.theme.colors.accent}; - vertical-align: middle; - margin: 0; - } - - .button-container { - flex-shrink: 0; - display: flex; - gap: 8px; - } -`; - -export default Wrapper; diff --git a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index a27a17f39..a123798f3 100644 --- a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -1,479 +1,54 @@ import React, { useCallback, useRef, useMemo } from 'react'; import { TableVirtuoso } from 'react-virtuoso'; import cloneDeep from 'lodash/cloneDeep'; -import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons'; -import { useTheme } from 'providers/Theme'; import { useDispatch, useSelector } from 'react-redux'; -import MultiLineEditor from 'components/MultiLineEditor/index'; -import StyledWrapper from './StyledWrapper'; -import { uuid } from 'utils/common'; -import { useFormik } from 'formik'; -import * as Yup from 'yup'; -import { variableNameRegex } from 'utils/common/regex'; -import toast from 'react-hot-toast'; import { saveGlobalEnvironment, setGlobalEnvironmentDraft, clearGlobalEnvironmentDraft } from 'providers/ReduxStore/slices/global-environments'; -import { Tooltip } from 'react-tooltip'; -import { getGlobalEnvironmentVariables } from 'utils/collections'; -import Button from 'ui/Button'; - -const MIN_H = 35 * 2; - -const TableRow = React.memo(({ children, item }) => {children}, (prevProps, nextProps) => { - const prevUid = prevProps?.item?.variable?.uid; - const nextUid = nextProps?.item?.variable?.uid; - return prevUid === nextUid && prevProps.children === nextProps.children; -}); +import EnvironmentVariablesTable from 'components/EnvironmentVariablesTable'; const EnvironmentVariables = ({ environment, setIsModified, collection, searchQuery = '' }) => { const dispatch = useDispatch(); - const { storedTheme } = useTheme(); - const { globalEnvironments, activeGlobalEnvironmentUid, globalEnvironmentDraft } = useSelector( - (state) => state.globalEnvironments - ); + const { globalEnvironmentDraft } = useSelector((state) => state.globalEnvironments); const hasDraftForThisEnv = globalEnvironmentDraft?.environmentUid === environment.uid; - const [tableHeight, setTableHeight] = React.useState(MIN_H); - - const handleTotalHeightChanged = React.useCallback((h) => { - setTableHeight(h); - }, []); - - // Track environment changes for draft restoration - const prevEnvUidRef = React.useRef(null); - const mountedRef = React.useRef(false); - - let _collection = collection ? cloneDeep(collection) : {}; - - const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid }); - if (_collection) { - _collection.globalEnvironmentVariables = globalEnvironmentVariables; - } - - // Initial values based only on saved environment variables (not draft) - // Draft restoration happens in a separate effect to avoid infinite loops - const initialValues = React.useMemo(() => { - const vars = environment.variables || []; - return [ - ...vars, - { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - } - ]; - }, [environment.uid, environment.variables]); - - const formik = useFormik({ - enableReinitialize: true, - initialValues: initialValues, - validationSchema: Yup.array().of(Yup.object({ - enabled: Yup.boolean(), - name: Yup.string() - .when('$isLastRow', { - is: true, - then: (schema) => schema.optional(), - otherwise: (schema) => schema - .required('Name cannot be empty') - .matches(variableNameRegex, - 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.') - .trim() - }), - secret: Yup.boolean(), - type: Yup.string(), - uid: Yup.string(), - value: Yup.mixed().nullable() - })), - validate: (values) => { - const errors = {}; - values.forEach((variable, index) => { - const isLastRow = index === values.length - 1; - const isEmptyRow = !variable.name || variable.name.trim() === ''; - - if (isLastRow && isEmptyRow) { - return; - } - - if (!variable.name || variable.name.trim() === '') { - if (!errors[index]) errors[index] = {}; - errors[index].name = 'Name cannot be empty'; - } else if (!variableNameRegex.test(variable.name)) { - if (!errors[index]) errors[index] = {}; - errors[index].name = 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.'; - } - }); - return Object.keys(errors).length > 0 ? errors : {}; + const handleSave = useCallback( + (variables) => { + return dispatch(saveGlobalEnvironment({ environmentUid: environment.uid, variables: cloneDeep(variables) })); }, - onSubmit: () => {} - }); + [dispatch, environment.uid] + ); - // Restore draft values on mount or environment switch - React.useEffect(() => { - const isMount = !mountedRef.current; - const envChanged = prevEnvUidRef.current !== null && prevEnvUidRef.current !== environment.uid; - - prevEnvUidRef.current = environment.uid; - mountedRef.current = true; - - if ((isMount || envChanged) && hasDraftForThisEnv && globalEnvironmentDraft?.variables) { - formik.setValues([ - ...globalEnvironmentDraft.variables, - { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - } - ]); - } - }, [environment.uid, hasDraftForThisEnv, globalEnvironmentDraft?.variables]); - - // Sync draft state to Redux - React.useEffect(() => { - const currentValues = formik.values.filter((variable) => variable.name && variable.name.trim() !== ''); - const savedValues = environment.variables || []; - - const currentValuesJson = JSON.stringify(currentValues); - const savedValuesJson = JSON.stringify(savedValues); - const hasActualChanges = currentValuesJson !== savedValuesJson; - - setIsModified(hasActualChanges); - - // Get existing draft for comparison - const existingDraftVariables = hasDraftForThisEnv ? globalEnvironmentDraft?.variables : null; - const existingDraftJson = existingDraftVariables ? JSON.stringify(existingDraftVariables) : null; - - if (hasActualChanges) { - // Only dispatch if draft values are actually different - if (currentValuesJson !== existingDraftJson) { - dispatch(setGlobalEnvironmentDraft({ + const handleDraftChange = useCallback( + (variables) => { + dispatch( + setGlobalEnvironmentDraft({ environmentUid: environment.uid, - variables: currentValues - })); - } - } else if (hasDraftForThisEnv) { - dispatch(clearGlobalEnvironmentDraft()); - } - }, [formik.values, environment.variables, environment.uid, setIsModified, dispatch, hasDraftForThisEnv, globalEnvironmentDraft?.variables]); + variables + }) + ); + }, + [dispatch, environment.uid] + ); - const ErrorMessage = ({ name, index }) => { - const meta = formik.getFieldMeta(name); - const id = `error-${name}-${index}`; - - const isLastRow = index === formik.values.length - 1; - const variable = formik.values[index]; - const isEmptyRow = !variable?.name || variable.name.trim() === ''; - - if (isLastRow && isEmptyRow) { - return null; - } - - if (!meta.error || !meta.touched) { - return null; - } - return ( - - - - - ); - }; - - const handleRemoveVar = useCallback((id) => { - const currentValues = formik.values; - - if (!currentValues || currentValues.length === 0) { - return; - } - - const lastRow = currentValues[currentValues.length - 1]; - const isLastEmptyRow = lastRow?.uid === id && (!lastRow.name || lastRow.name.trim() === ''); - - if (isLastEmptyRow) { - return; - } - - const filteredValues = currentValues.filter((variable) => variable.uid !== id); - - const hasEmptyLastRow = filteredValues.length > 0 - && (!filteredValues[filteredValues.length - 1].name - || filteredValues[filteredValues.length - 1].name.trim() === ''); - - const newValues = hasEmptyLastRow - ? filteredValues - : [ - ...filteredValues, - { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - } - ]; - - formik.setValues(newValues); - }, [formik.values]); - - const handleNameChange = (index, e) => { - formik.handleChange(e); - const isLastRow = index === formik.values.length - 1; - - if (isLastRow) { - const newVariable = { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - }; - setTimeout(() => { - formik.setFieldValue(formik.values.length, newVariable, false); - }, 0); - } - }; - - const handleNameBlur = (index) => { - formik.setFieldTouched(`${index}.name`, true, true); - }; - - const handleNameKeyDown = (index, e) => { - if (e.key === 'Enter') { - e.preventDefault(); - formik.setFieldTouched(`${index}.name`, true, true); - } - }; - - const handleSave = () => { - const variablesToSave = formik.values.filter((variable) => variable.name && variable.name.trim() !== ''); - const savedValues = environment.variables || []; - - const hasChanges = JSON.stringify(variablesToSave) !== JSON.stringify(savedValues); - if (!hasChanges) { - toast.error('No changes to save'); - return; - } - - const hasValidationErrors = variablesToSave.some((variable) => { - if (!variable.name || variable.name.trim() === '') { - return true; - } - if (!variableNameRegex.test(variable.name)) { - return true; - } - return false; - }); - - if (hasValidationErrors) { - toast.error('Please fix validation errors before saving'); - return; - } - - dispatch(saveGlobalEnvironment({ environmentUid: environment.uid, variables: cloneDeep(variablesToSave) })) - .then(() => { - toast.success('Changes saved successfully'); - const newValues = [ - ...variablesToSave, - { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - } - ]; - formik.resetForm({ values: newValues }); - setIsModified(false); - }) - .catch((error) => { - console.error(error); - toast.error('An error occurred while saving the changes'); - }); - }; - - const handleReset = () => { - const originalVars = environment.variables || []; - const resetValues = [ - ...originalVars, - { - uid: uuid(), - name: '', - value: '', - type: 'text', - secret: false, - enabled: true - } - ]; - formik.resetForm({ values: resetValues }); - setIsModified(false); - }; - - const handleSaveRef = useRef(handleSave); - handleSaveRef.current = handleSave; - - React.useEffect(() => { - const handleSaveEvent = () => { - handleSaveRef.current(); - }; - - window.addEventListener('environment-save', handleSaveEvent); - - return () => { - window.removeEventListener('environment-save', handleSaveEvent); - }; - }, []); - - const filteredVariables = useMemo(() => { - const allVariables = formik.values.map((variable, index) => ({ variable, index })); - if (!searchQuery?.trim()) { - return allVariables; - } - - const query = searchQuery.toLowerCase().trim(); - - return allVariables.filter(({ variable, index }) => { - const isLastRow = index === formik.values.length - 1; - const isEmptyRow = !variable.name || variable.name.trim() === ''; - if (isLastRow && isEmptyRow) { - return true; - } - - const nameMatch = variable.name ? variable.name.toLowerCase().includes(query) : false; - const valueMatch = typeof variable.value === 'string' ? variable.value.toLowerCase().includes(query) : false; - - return !!(nameMatch || valueMatch); - }); - }, [formik.values, searchQuery]); + const handleDraftClear = useCallback(() => { + dispatch(clearGlobalEnvironmentDraft()); + }, [dispatch]); return ( - - item.variable.uid} - fixedHeaderContent={() => ( - - - - - - - - )} - - itemContent={(index, { variable, index: actualIndex }) => { - const isLastRow = actualIndex === formik.values.length - 1; - const isEmptyRow = !variable.name || variable.name.trim() === ''; - const isLastEmptyRow = isLastRow && isEmptyRow; - - return ( - <> - - - - - - - ); - }} - /> - -
-
- - -
-
- + ); };
{checkboxLabel} - {column.name} + {column.name} + {colIndex < columns.length - 1 && ( +
0 ? `${tableHeight}px` : undefined }} + onMouseDown={(e) => handleResizeStart(e, column.key)} + /> + )}
+ Name +
0 ? `${tableHeight}px` : undefined }} + onMouseDown={(e) => handleResizeStart(e, 'name')} + /> +
ValueSecret
+ {!isLastEmptyRow && ( + + )} + +
+ handleNameChange(actualIndex, e)} + onBlur={() => handleNameBlur(actualIndex)} + onKeyDown={(e) => handleNameKeyDown(actualIndex, e)} + /> + +
+
+
+ formik.setFieldValue(`${actualIndex}.value`, newValue, true)} + onSave={handleSave} + /> +
+ {typeof variable.value !== 'string' && ( + + + + + )} + {renderExtraValueContent && renderExtraValueContent(variable)} +
+ {!isLastEmptyRow && ( + + )} + + {!isLastEmptyRow && ( + + )} +
NameValueSecret
- {!isLastEmptyRow && ( - - )} - -
- handleNameChange(actualIndex, e)} - onBlur={() => handleNameBlur(actualIndex)} - onKeyDown={(e) => handleNameKeyDown(actualIndex, e)} - /> - -
-
-
- formik.setFieldValue(`${actualIndex}.value`, newValue, true)} - onSave={handleSave} - /> -
- {typeof variable.value !== 'string' && ( - - - - - )} - {!variable.secret && hasSensitiveUsage(variable.name) && ( - - )} -
- {!isLastEmptyRow && ( - - )} - - {!isLastEmptyRow && ( - - )} -
NameValueSecret
- {!isLastEmptyRow && ( - - )} - -
- handleNameChange(actualIndex, e)} - onBlur={() => handleNameBlur(actualIndex)} - onKeyDown={(e) => handleNameKeyDown(actualIndex, e)} - /> - -
-
-
- formik.setFieldValue(`${actualIndex}.value`, newValue, true)} - onSave={handleSave} - /> -
- {typeof variable.value !== 'string' && ( - - - - - )} -
- {!isLastEmptyRow && ( - - )} - - {!isLastEmptyRow && ( - - )} -