mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
Environment's as tabs (#6407)
* add: env's as tabs * fix: test * fix: tests * fixes * fix: test * fixes * fixes * fix * fix: styling * fixes
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
import { IconBox, IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
|
||||
import { IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
|
||||
import { areItemsLoading, getItemsLoadStats } from 'utils/collections/index';
|
||||
import { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import ShareCollection from 'components/ShareCollection/index';
|
||||
import { updateEnvironmentSettingsModalVisibility, updateGlobalEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
|
||||
const Info = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -53,7 +53,13 @@ const Info = ({ collection }) => {
|
||||
type="button"
|
||||
className="text-sm text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
onClick={() => {
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(true));
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{collectionEnvironmentCount} collection environment{collectionEnvironmentCount !== 1 ? 's' : ''}
|
||||
@@ -61,7 +67,15 @@ const Info = ({ collection }) => {
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
onClick={() => dispatch(updateGlobalEnvironmentSettingsModalVisibility(true))}
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-global-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'global-environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{globalEnvironmentCount} global environment{globalEnvironmentCount !== 1 ? 's' : ''}
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import Portal from 'components/Portal';
|
||||
|
||||
const ConfirmCloseEnvironment = ({ onCancel, onCloseWithoutSave, onSaveAndClose, isGlobal }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="md"
|
||||
title="Unsaved changes"
|
||||
disableEscapeKey={true}
|
||||
disableCloseOnOutsideClick={true}
|
||||
closeModalFadeTimeout={150}
|
||||
handleCancel={onCancel}
|
||||
hideFooter={true}
|
||||
>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-medium">Hold on...</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
You have unsaved changes in {isGlobal ? 'global' : 'collection'} environment settings.
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCloseWithoutSave}>
|
||||
Don't Save
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={onSaveAndClose}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmCloseEnvironment;
|
||||
@@ -1,18 +1,16 @@
|
||||
import React, { useMemo, useState, useRef, forwardRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { IconWorld, IconDatabase, IconCaretDown, IconSettings, IconPlus, IconDownload } from '@tabler/icons';
|
||||
import { IconWorld, IconDatabase, IconCaretDown } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateEnvironmentSettingsModalVisibility, updateGlobalEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { selectGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import toast from 'react-hot-toast';
|
||||
import EnvironmentListContent from './EnvironmentListContent/index';
|
||||
import EnvironmentSettings from '../EnvironmentSettings';
|
||||
import GlobalEnvironmentSettings from 'components/GlobalEnvironments/EnvironmentSettings';
|
||||
import CreateEnvironment from '../EnvironmentSettings/CreateEnvironment';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import CreateGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment';
|
||||
import CreateGlobalEnvironment from 'components/WorkspaceHome/WorkspaceEnvironments/CreateEnvironment';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -27,8 +25,6 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
|
||||
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
|
||||
const activeGlobalEnvironmentUid = useSelector((state) => state.globalEnvironments.activeGlobalEnvironmentUid);
|
||||
const isEnvironmentSettingsModalOpen = useSelector((state) => state.app.isEnvironmentSettingsModalOpen);
|
||||
const isGlobalEnvironmentSettingsModalOpen = useSelector((state) => state.app.isGlobalEnvironmentSettingsModalOpen);
|
||||
const activeGlobalEnvironment = activeGlobalEnvironmentUid
|
||||
? find(globalEnvironments, (e) => e.uid === activeGlobalEnvironmentUid)
|
||||
: null;
|
||||
@@ -75,12 +71,24 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Settings handler
|
||||
// Settings handler - opens environment settings tab
|
||||
const handleSettingsClick = () => {
|
||||
if (activeTab === 'collection') {
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(true));
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'environment-settings'
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(updateGlobalEnvironmentSettingsModalVisibility(true));
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-global-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'global-environment-settings'
|
||||
})
|
||||
);
|
||||
}
|
||||
dropdownTippyRef.current.hide();
|
||||
};
|
||||
@@ -105,12 +113,6 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
};
|
||||
|
||||
// Modal handlers
|
||||
const handleCloseSettings = () => {
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(false));
|
||||
dispatch(updateGlobalEnvironmentSettingsModalVisibility(false));
|
||||
};
|
||||
|
||||
// Calculate dropdown width based on the longest environment name.
|
||||
// To prevent resizing while switching between collection and global environments.
|
||||
const dropdownWidth = useMemo(() => {
|
||||
@@ -217,25 +219,17 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
{/* Modals - Rendered outside dropdown to avoid conflicts */}
|
||||
{isGlobalEnvironmentSettingsModalOpen && (
|
||||
<GlobalEnvironmentSettings
|
||||
globalEnvironments={globalEnvironments}
|
||||
collection={collection}
|
||||
activeGlobalEnvironmentUid={activeGlobalEnvironmentUid}
|
||||
onClose={handleCloseSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isEnvironmentSettingsModalOpen && (
|
||||
<EnvironmentSettings collection={collection} onClose={handleCloseSettings} />
|
||||
)}
|
||||
|
||||
{showCreateGlobalModal && (
|
||||
<CreateGlobalEnvironment
|
||||
onClose={() => setShowCreateGlobalModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
dispatch(updateGlobalEnvironmentSettingsModalVisibility(true));
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-global-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'global-environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -245,7 +239,13 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
type="global"
|
||||
onClose={() => setShowImportGlobalModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
dispatch(updateGlobalEnvironmentSettingsModalVisibility(true));
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-global-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'global-environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -255,7 +255,13 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
collection={collection}
|
||||
onClose={() => setShowCreateCollectionModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(true));
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -266,7 +272,13 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
collection={collection}
|
||||
onClose={() => setShowImportCollectionModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(true));
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
const ConfirmSwitchEnv = ({ onCancel }) => {
|
||||
return createPortal(
|
||||
<Modal
|
||||
size="md"
|
||||
title="Unsaved changes"
|
||||
confirmText="Save and Close"
|
||||
cancelText="Close without saving"
|
||||
disableEscapeKey={true}
|
||||
disableCloseOnOutsideClick={true}
|
||||
closeModalFadeTimeout={150}
|
||||
handleCancel={onCancel}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
hideFooter={true}
|
||||
>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-medium">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">You have unsaved changes in this environment.</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCancel}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</Modal>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmSwitchEnv;
|
||||
@@ -1,67 +1,192 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.table-container {
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
border: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 500;
|
||||
table-layout: fixed;
|
||||
font-size: 12px;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
|
||||
padding: 4px 10px;
|
||||
vertical-align: middle;
|
||||
padding: 2px 10px;
|
||||
|
||||
&:nth-child(1),
|
||||
&:nth-child(1) {
|
||||
width: 25px;
|
||||
border-right: none;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
width: 70px;
|
||||
width: 80px;
|
||||
}
|
||||
&:nth-child(5) {
|
||||
width: 40px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 25%;
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
color: ${(props) => props.theme.colors.text};
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
user-select: none;
|
||||
|
||||
td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-right: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
thead td {
|
||||
padding: 6px 10px;
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
transition: background 0.1s ease;
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-right: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-param {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.textLink};
|
||||
font-weight: 500;
|
||||
padding: 7px 14px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 6px;
|
||||
border: ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
background: transparent;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.listItem.hoverBg};
|
||||
border-color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-mod {
|
||||
font-size: ${(props) => props.theme.font.size.xs} !important;
|
||||
width: 150px !important;
|
||||
font-size: 11px !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
border: 1px solid transparent;
|
||||
outline: none !important;
|
||||
background-color: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: ${(props) => props.theme.workspace.accent};
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding: 12px 2px;
|
||||
background: ${(props) => props.theme.bg};
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.submit {
|
||||
padding: 7px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background: ${(props) => props.theme.workspace.accent};
|
||||
color: ${(props) => props.theme.bg};
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.reset {
|
||||
background: transparent;
|
||||
padding: 6px 16px;
|
||||
border: 1px solid ${(props) => props.theme.workspace.accent};
|
||||
color: ${(props) => props.theme.workspace.accent};
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.discard {
|
||||
padding: 7px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
border: ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
import React, { useRef, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useRef, useMemo, useEffect } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { get } from 'lodash';
|
||||
import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
|
||||
import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
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 { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { setEnvironmentsDraft, clearEnvironmentsDraft } from 'providers/ReduxStore/slices/collections';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { getGlobalEnvironmentVariables, flattenItems, isItemARequest } from 'utils/collections';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { sensitiveFields } from './constants';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
|
||||
const EnvironmentVariables = ({ environment, setIsModified, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const addButtonRef = useRef(null);
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
|
||||
let _collection = cloneDeep(collection);
|
||||
const environmentsDraft = collection?.environmentsDraft;
|
||||
const hasDraftForThisEnv = environmentsDraft?.environmentUid === environment.uid;
|
||||
|
||||
// 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 });
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
if (_collection) {
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
}
|
||||
|
||||
// Check for non-secret variables used in sensitive fields
|
||||
const nonSecretSensitiveVarUsageMap = useMemo(() => {
|
||||
const result = {};
|
||||
if (!collection || !environment?.variables) {
|
||||
@@ -44,7 +53,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
const value = get(obj, fieldPath);
|
||||
if (typeof value === 'string') {
|
||||
varNames.forEach((varName) => {
|
||||
if (new RegExp(`\{\{\s*${varName}\s*\}\}`).test(value)) {
|
||||
if (new RegExp(`\\{\\{\\s*${varName}\\s*\\}\\}`).test(value)) {
|
||||
result[varName] = true;
|
||||
}
|
||||
});
|
||||
@@ -73,51 +82,147 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
return result;
|
||||
}, [collection, environment]);
|
||||
|
||||
const hasSensitiveUsage = (name) => !!nonSecretSensitiveVarUsageMap[name];
|
||||
|
||||
// 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: environment.variables || [],
|
||||
initialValues: initialValues,
|
||||
validationSchema: Yup.array().of(
|
||||
Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
name: Yup.string()
|
||||
.required('Name cannot be empty')
|
||||
.matches(
|
||||
variableNameRegex,
|
||||
'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.'
|
||||
)
|
||||
.trim(),
|
||||
.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.string().trim().nullable()
|
||||
value: Yup.mixed().nullable()
|
||||
})
|
||||
),
|
||||
onSubmit: (values) => {
|
||||
if (!formik.dirty) {
|
||||
toast.error('Nothing to save');
|
||||
return;
|
||||
}
|
||||
validate: (values) => {
|
||||
const errors = {};
|
||||
values.forEach((variable, index) => {
|
||||
const isLastRow = index === values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
|
||||
dispatch(saveEnvironment(cloneDeep(values), environment.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Changes saved successfully');
|
||||
formik.resetForm({ values });
|
||||
setIsModified(false);
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while saving the changes'));
|
||||
}
|
||||
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: () => {}
|
||||
});
|
||||
|
||||
const hasSensitiveUsage = (name) => !!nonSecretSensitiveVarUsageMap[name];
|
||||
// Restore draft values on mount or environment switch
|
||||
useEffect(() => {
|
||||
const isMount = !mountedRef.current;
|
||||
const envChanged = prevEnvUidRef.current !== null && prevEnvUidRef.current !== environment.uid;
|
||||
|
||||
// Effect to track modifications.
|
||||
React.useEffect(() => {
|
||||
setIsModified(formik.dirty);
|
||||
}, [formik.dirty]);
|
||||
prevEnvUidRef.current = environment.uid;
|
||||
mountedRef.current = true;
|
||||
|
||||
const ErrorMessage = ({ name }) => {
|
||||
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 }));
|
||||
}
|
||||
}, 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 = uuid();
|
||||
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;
|
||||
}
|
||||
@@ -129,54 +234,161 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
);
|
||||
};
|
||||
|
||||
const addVariable = () => {
|
||||
const newVariable = {
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
};
|
||||
formik.setFieldValue(formik.values.length, newVariable, false);
|
||||
};
|
||||
const handleRemoveVar = useCallback((id) => {
|
||||
const currentValues = formik.values;
|
||||
|
||||
const onActivate = () => {
|
||||
dispatch(selectEnvironment(environment ? environment.uid : null, collection.uid))
|
||||
.then(() => {
|
||||
if (environment) {
|
||||
toast.success(`Environment changed to ${environment.name}`);
|
||||
onClose();
|
||||
} else {
|
||||
toast.success(`No Environments are active now`);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment'));
|
||||
};
|
||||
|
||||
const handleRemoveVar = (id) => {
|
||||
formik.setValues(formik.values.filter((variable) => variable.uid !== id));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (formik.dirty) {
|
||||
// Smooth scrolling to the changed parameter is temporarily disabled
|
||||
// due to UX issues when editing the first row in a long list of environment variables.
|
||||
// addButtonRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
if (!currentValues || currentValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
}, [formik.values, formik.dirty]);
|
||||
|
||||
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 = () => {
|
||||
formik.resetForm({ originalEnvironmentVariables });
|
||||
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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-6 mb-6">
|
||||
<div className="h-[50vh] overflow-y-auto w-full">
|
||||
<table className="environment-variables">
|
||||
<StyledWrapper>
|
||||
<div className="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td className="text-center">Enabled</td>
|
||||
<td className="text-center"></td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td className="text-center">Secret</td>
|
||||
@@ -184,99 +396,112 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formik.values.map((variable, index) => (
|
||||
<tr key={variable.uid} data-testid={`env-var-row-${variable.name}`}>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<MultiLineEditor
|
||||
theme={storedTheme}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
isSecret={variable.secret}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
enableBrunoVarInfo={false}
|
||||
/>
|
||||
</div>
|
||||
{!variable.secret && hasSensitiveUsage(variable.name) && (
|
||||
<SensitiveFieldWarning
|
||||
fieldName={variable.name}
|
||||
warningMessage="This variable is used in sensitive fields. Mark it as a secret for security"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{formik.values.map((variable, index) => {
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
const isLastEmptyRow = isLastRow && isEmptyRow;
|
||||
|
||||
return (
|
||||
<tr key={variable.uid} data-testid={`env-var-row-${variable.name}`}>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
placeholder={isLastEmptyRow ? 'Name' : ''}
|
||||
onChange={(e) => handleNameChange(index, e)}
|
||||
onBlur={() => handleNameBlur(index)}
|
||||
onKeyDown={(e) => handleNameKeyDown(index, e)}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} index={index} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<MultiLineEditor
|
||||
theme={storedTheme}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
isSecret={variable.secret}
|
||||
readOnly={typeof variable.value !== 'string'}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
{typeof variable.value !== 'string' && (
|
||||
<span className="ml-2 flex items-center">
|
||||
<IconInfoCircle id={`${variable.uid}-disabled-info-icon`} className="text-muted" size={16} />
|
||||
<Tooltip
|
||||
anchorId={`${variable.uid}-disabled-info-icon`}
|
||||
content="Non-string values set via scripts are read-only and can only be updated through scripts."
|
||||
place="top"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{!variable.secret && hasSensitiveUsage(variable.name) && (
|
||||
<SensitiveFieldWarning
|
||||
fieldName={variable.name}
|
||||
warningMessage="This variable is used in sensitive fields. Mark it as a secret for security"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{!isLastEmptyRow && (
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={18} />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button
|
||||
ref={addButtonRef}
|
||||
className="btn-add-param text-link pr-2 py-3 mt-2 select-none"
|
||||
onClick={addVariable}
|
||||
id="add-variable"
|
||||
data-testid="add-variable"
|
||||
>
|
||||
+ Add Variable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-2 flex items-center" onClick={formik.handleSubmit} data-testid="save-env">
|
||||
<IconDeviceFloppy size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Save
|
||||
</button>
|
||||
<button type="submit" className="ml-2 px-1 submit btn btn-sm btn-close mt-2 flex items-center" onClick={handleReset} data-testid="reset-env">
|
||||
<IconRefresh size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Reset
|
||||
</button>
|
||||
<button type="submit" className="submit btn btn-sm btn-close mt-2 flex items-center" onClick={onActivate} data-testid="activate-env">
|
||||
<IconCircleCheck size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Activate
|
||||
</button>
|
||||
<div className="button-container">
|
||||
<div className="flex items-center">
|
||||
<button type="button" className="submit" onClick={handleSave} data-testid="save-env">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" className="submit reset ml-2" onClick={handleReset} data-testid="reset-env">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentVariables;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.bg};
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px 8px 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.text};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
|
||||
&.renaming {
|
||||
.title-input {
|
||||
flex: 1;
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
outline: none;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.inline-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.inline-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&.save {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
&.cancel {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-error {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 20px;
|
||||
margin-top: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background: ${(props) => props.theme.bg};
|
||||
border: 1px solid ${(props) => props.theme.colors.text.danger};
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&:last-child:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,59 +1,183 @@
|
||||
import { IconCopy, IconDatabase, IconEdit, IconTrash } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
import CopyEnvironment from '../../CopyEnvironment';
|
||||
import DeleteEnvironment from '../../DeleteEnvironment';
|
||||
import RenameEnvironment from '../../RenameEnvironment';
|
||||
import { IconCopy, IconEdit, IconTrash, IconCheck, IconX } from '@tabler/icons';
|
||||
import { useState, useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
import CopyEnvironment from 'components/Environments/EnvironmentSettings/CopyEnvironment';
|
||||
import DeleteEnvironment from 'components/Environments/EnvironmentSettings/DeleteEnvironment';
|
||||
import EnvironmentVariables from './EnvironmentVariables';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const environments = collection?.environments || [];
|
||||
|
||||
const EnvironmentDetails = ({ environment, collection, setIsModified, onClose }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
const [openCopyModal, setOpenCopyModal] = useState(false);
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const validateEnvironmentName = (name) => {
|
||||
if (!name || name.trim() === '') {
|
||||
return 'Name is required';
|
||||
}
|
||||
|
||||
if (name.length < 1) {
|
||||
return 'Must be at least 1 character';
|
||||
}
|
||||
|
||||
if (name.length > 255) {
|
||||
return 'Must be 255 characters or less';
|
||||
}
|
||||
|
||||
if (!validateName(name)) {
|
||||
return validateNameError(name);
|
||||
}
|
||||
|
||||
const trimmedName = name.toLowerCase().trim();
|
||||
const isDuplicate = (environments || []).some(
|
||||
(env) => env?.uid !== environment.uid && env?.name?.toLowerCase().trim() === trimmedName
|
||||
);
|
||||
if (isDuplicate) {
|
||||
return 'Environment already exists';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleRenameClick = () => {
|
||||
setIsRenaming(true);
|
||||
setNewName(environment.name);
|
||||
setNameError('');
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleSaveRename = () => {
|
||||
const error = validateEnvironmentName(newName);
|
||||
if (error) {
|
||||
setNameError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(renameEnvironment(newName, environment.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment renamed!');
|
||||
setIsRenaming(false);
|
||||
setNewName('');
|
||||
setNameError('');
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('An error occurred while renaming the environment');
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancelRename = () => {
|
||||
setIsRenaming(false);
|
||||
setNewName('');
|
||||
setNameError('');
|
||||
};
|
||||
|
||||
const handleNameChange = (e) => {
|
||||
setNewName(e.target.value);
|
||||
if (nameError) {
|
||||
setNameError('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleNameBlur = () => {
|
||||
if (newName.trim() === '') {
|
||||
handleCancelRename();
|
||||
} else {
|
||||
const error = validateEnvironmentName(newName);
|
||||
if (error) {
|
||||
setNameError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleNameKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSaveRename();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleCancelRename();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
|
||||
{openEditModal && (
|
||||
<RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection} />
|
||||
)}
|
||||
<StyledWrapper>
|
||||
{openDeleteModal && (
|
||||
<DeleteEnvironment
|
||||
onClose={() => setOpenDeleteModal(false)}
|
||||
environment={environment}
|
||||
collection={collection}
|
||||
/>
|
||||
<DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={environment} collection={collection} />
|
||||
)}
|
||||
{openCopyModal && (
|
||||
<CopyEnvironment onClose={() => setOpenCopyModal(false)} environment={environment} collection={collection} />
|
||||
)}
|
||||
<div className="flex">
|
||||
<div className="flex flex-grow items-center">
|
||||
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
<span className="ml-1 font-medium break-all">{environment.name}</span>
|
||||
|
||||
<div className="header">
|
||||
<div className={`title-container ${isRenaming ? 'renaming' : ''}`}>
|
||||
{isRenaming ? (
|
||||
<>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="title-input"
|
||||
value={newName}
|
||||
onChange={handleNameChange}
|
||||
onBlur={handleNameBlur}
|
||||
onKeyDown={handleNameKeyDown}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div className="inline-actions">
|
||||
<button
|
||||
className="inline-action-btn save"
|
||||
onClick={handleSaveRename}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title="Save"
|
||||
>
|
||||
<IconCheck size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
className="inline-action-btn cancel"
|
||||
onClick={handleCancelRename}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title="Cancel"
|
||||
>
|
||||
<IconX size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<h2 className="title">{environment.name}</h2>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-x-2 pl-2">
|
||||
<ToolHint text="Edit Environment" toolhintId={`edit-${environment.uid}`}>
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
|
||||
</ToolHint>
|
||||
<ToolHint text="Copy Environment" toolhintId={`copy-${environment.uid}`}>
|
||||
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
|
||||
</ToolHint>
|
||||
<ToolHint text="Delete Environment" toolhintId={`delete-${environment.uid}`}>
|
||||
<IconTrash
|
||||
className="cursor-pointer"
|
||||
size={20}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => setOpenDeleteModal(true)}
|
||||
data-testid="delete-environment-button"
|
||||
/>
|
||||
</ToolHint>
|
||||
{nameError && isRenaming && <div className="title-error">{nameError}</div>}
|
||||
<div className="actions">
|
||||
<button onClick={handleRenameClick} title="Rename">
|
||||
<IconEdit size={15} strokeWidth={1.5} />
|
||||
</button>
|
||||
<button onClick={() => setOpenCopyModal(true)} title="Copy">
|
||||
<IconCopy size={15} strokeWidth={1.5} />
|
||||
</button>
|
||||
<button onClick={() => setOpenDeleteModal(true)} title="Delete">
|
||||
<IconTrash size={15} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<EnvironmentVariables environment={environment} collection={collection} setIsModified={setIsModified} onClose={onClose} />
|
||||
<div className="content">
|
||||
<EnvironmentVariables environment={environment} setIsModified={setIsModified} collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,61 +1,281 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
margin-inline: -1rem;
|
||||
margin-block: -1.5rem;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
position: relative;
|
||||
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.bg};
|
||||
|
||||
.environments-sidebar {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
|
||||
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
||||
min-height: 400px;
|
||||
.environments-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
max-height: 85vh;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.confirm-switch-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
background: ${(props) => props.theme.bg};
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
}
|
||||
|
||||
/* Left Sidebar */
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
border-right: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 16px 12px 16px;
|
||||
|
||||
.title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.text};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-container {
|
||||
position: relative;
|
||||
padding: 0 12px 12px 12px;
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-100%);
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 6px 8px 6px 28px;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
border: ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
border-radius: 5px;
|
||||
color: ${(props) => props.theme.text};
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.environments-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.environment-item {
|
||||
min-width: 150px;
|
||||
display: block;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 8px;
|
||||
margin-bottom: 1px;
|
||||
font-size: 13px;
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
border-left: solid 2px transparent;
|
||||
text-decoration: none;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: 5px;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
.environment-name {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.environment-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
.activate-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.text.muted};
|
||||
border-radius: 3px;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.workspace.button.bg};
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
}
|
||||
|
||||
.activated-checkmark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .environment-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.activated .environment-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
|
||||
background: ${(props) => props.theme.workspace.button.bg};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.workspace.environments.activeBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&.renaming,
|
||||
&.creating {
|
||||
cursor: default;
|
||||
padding: 4px 4px 4px 8px;
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.workspace.button.bg};
|
||||
}
|
||||
}
|
||||
|
||||
.rename-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
.environment-name-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 13px;
|
||||
padding: 2px 4px;
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
.inline-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.creating {
|
||||
.environment-name-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 13px;
|
||||
padding: 2px 4px;
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
.inline-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.inline-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&.save {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.listItem.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
&.cancel {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.listItem.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
|
||||
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-create-environment,
|
||||
.btn-import-environment {
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
color: ${(props) => props.theme.textLink};
|
||||
|
||||
span:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-import-environment {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
.env-error {
|
||||
padding: 4px 12px;
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background: ${(props) => `${props.theme.colors.text.danger}15`};
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,21 +1,39 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import usePrevious from 'hooks/usePrevious';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment';
|
||||
import { IconDownload, IconShieldLock, IconUpload } from '@tabler/icons';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import ManageSecrets from '../ManageSecrets';
|
||||
import CreateEnvironment from 'components/Environments/EnvironmentSettings/CreateEnvironment';
|
||||
import { IconDownload, IconUpload, IconSearch, IconPlus, IconCheck, IconX } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ConfirmSwitchEnv from './ConfirmSwitchEnv';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import ConfirmSwitchEnv from 'components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/ConfirmSwitchEnv';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addEnvironment, renameEnvironment, selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const EnvironmentList = ({
|
||||
environments,
|
||||
activeEnvironmentUid,
|
||||
selectedEnvironment,
|
||||
setSelectedEnvironment,
|
||||
isModified,
|
||||
setIsModified,
|
||||
collection,
|
||||
setShowExportModal
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified, onClose, setShowExportModal }) => {
|
||||
const { environments } = collection;
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openImportModal, setOpenImportModal] = useState(false);
|
||||
const [openManageSecretsModal, setOpenManageSecretsModal] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [isCreatingInline, setIsCreatingInline] = useState(false);
|
||||
const [renamingEnvUid, setRenamingEnvUid] = useState(null);
|
||||
const [newEnvName, setNewEnvName] = useState('');
|
||||
const [envNameError, setEnvNameError] = useState('');
|
||||
const inputRef = useRef(null);
|
||||
const renameContainerRef = useRef(null);
|
||||
const createContainerRef = useRef(null);
|
||||
|
||||
const [switchEnvConfirmClose, setSwitchEnvConfirmClose] = useState(false);
|
||||
const [originalEnvironmentVariables, setOriginalEnvironmentVariables] = useState([]);
|
||||
@@ -24,23 +42,36 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
const prevEnvUids = usePrevious(envUids);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEnvironment) {
|
||||
const _selectedEnvironment = environments?.find((env) => env?.uid === selectedEnvironment?.uid);
|
||||
const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);
|
||||
if (hasSelectedEnvironmentChanged) {
|
||||
setSelectedEnvironment(_selectedEnvironment);
|
||||
}
|
||||
setOriginalEnvironmentVariables(selectedEnvironment.variables);
|
||||
if (!environments?.length) {
|
||||
setSelectedEnvironment(null);
|
||||
setOriginalEnvironmentVariables([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
if (environment) {
|
||||
setSelectedEnvironment(environment);
|
||||
} else {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
if (selectedEnvironment) {
|
||||
let _selectedEnvironment = environments?.find((env) => env?.uid === selectedEnvironment?.uid);
|
||||
|
||||
if (!_selectedEnvironment) {
|
||||
_selectedEnvironment = environments?.find((env) => env?.name === selectedEnvironment?.name);
|
||||
}
|
||||
|
||||
if (!_selectedEnvironment) {
|
||||
_selectedEnvironment = environments?.find((env) => env.uid === activeEnvironmentUid) || environments?.[0];
|
||||
}
|
||||
|
||||
const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);
|
||||
if (hasSelectedEnvironmentChanged || selectedEnvironment.uid !== _selectedEnvironment?.uid) {
|
||||
setSelectedEnvironment(_selectedEnvironment);
|
||||
}
|
||||
setOriginalEnvironmentVariables(_selectedEnvironment?.variables || []);
|
||||
return;
|
||||
}
|
||||
}, [collection, environments, selectedEnvironment]);
|
||||
|
||||
const environment = environments?.find((env) => env.uid === activeEnvironmentUid) || environments?.[0];
|
||||
|
||||
setSelectedEnvironment(environment);
|
||||
setOriginalEnvironmentVariables(environment?.variables || []);
|
||||
}, [environments, activeEnvironmentUid, selectedEnvironment]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) {
|
||||
@@ -55,6 +86,36 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
}
|
||||
}, [envUids, environments, prevEnvUids]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!renamingEnvUid) return;
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (renameContainerRef.current && !renameContainerRef.current.contains(event.target)) {
|
||||
handleCancelRename();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [renamingEnvUid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCreatingInline) return;
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (createContainerRef.current && !createContainerRef.current.contains(event.target)) {
|
||||
handleCancelCreate();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isCreatingInline]);
|
||||
|
||||
const handleEnvironmentClick = (env) => {
|
||||
if (!isModified) {
|
||||
setSelectedEnvironment(env);
|
||||
@@ -63,18 +124,141 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnvironmentDoubleClick = (env) => {
|
||||
setRenamingEnvUid(env.uid);
|
||||
setNewEnvName(env.name);
|
||||
setEnvNameError('');
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleActivateEnvironment = (e, env) => {
|
||||
e.stopPropagation();
|
||||
dispatch(selectEnvironment(env.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success(`Environment "${env.name}" activated`);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Failed to activate environment');
|
||||
});
|
||||
};
|
||||
|
||||
if (!selectedEnvironment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const validateEnvironmentName = (name, excludeUid = null) => {
|
||||
if (!name || name.trim() === '') {
|
||||
return 'Name is required';
|
||||
}
|
||||
|
||||
if (!validateName(name)) {
|
||||
return validateNameError(name);
|
||||
}
|
||||
|
||||
const trimmedName = name.toLowerCase().trim();
|
||||
const isDuplicate = environments.some(
|
||||
(env) => env?.uid !== excludeUid && env?.name?.toLowerCase().trim() === trimmedName
|
||||
);
|
||||
if (isDuplicate) {
|
||||
return 'Environment already exists';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleCreateEnvClick = () => {
|
||||
if (!isModified) {
|
||||
setOpenCreateModal(true);
|
||||
setIsCreatingInline(true);
|
||||
setNewEnvName('');
|
||||
setEnvNameError('');
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 50);
|
||||
} else {
|
||||
setSwitchEnvConfirmClose(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelCreate = () => {
|
||||
setIsCreatingInline(false);
|
||||
setNewEnvName('');
|
||||
setEnvNameError('');
|
||||
};
|
||||
|
||||
const handleSaveNewEnv = () => {
|
||||
const error = validateEnvironmentName(newEnvName);
|
||||
if (error) {
|
||||
setEnvNameError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(addEnvironment(newEnvName, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment created!');
|
||||
setIsCreatingInline(false);
|
||||
setNewEnvName('');
|
||||
setEnvNameError('');
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('An error occurred while creating the environment');
|
||||
});
|
||||
};
|
||||
|
||||
const handleEnvNameChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setNewEnvName(value);
|
||||
|
||||
if (envNameError) {
|
||||
setEnvNameError('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnvNameKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (renamingEnvUid) {
|
||||
handleSaveRename();
|
||||
} else {
|
||||
handleSaveNewEnv();
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
if (renamingEnvUid) {
|
||||
handleCancelRename();
|
||||
} else {
|
||||
handleCancelCreate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveRename = () => {
|
||||
const error = validateEnvironmentName(newEnvName, renamingEnvUid);
|
||||
if (error) {
|
||||
setEnvNameError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(renameEnvironment(newEnvName, renamingEnvUid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment renamed!');
|
||||
setRenamingEnvUid(null);
|
||||
setNewEnvName('');
|
||||
setEnvNameError('');
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('An error occurred while renaming the environment');
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancelRename = () => {
|
||||
setRenamingEnvUid(null);
|
||||
setNewEnvName('');
|
||||
setEnvNameError('');
|
||||
};
|
||||
|
||||
const handleImportClick = () => {
|
||||
if (!isModified) {
|
||||
setOpenImportModal(true);
|
||||
@@ -83,8 +267,10 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecretsClick = () => {
|
||||
setOpenManageSecretsModal(true);
|
||||
const handleExportClick = () => {
|
||||
if (setShowExportModal) {
|
||||
setShowExportModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSwitch = (saveChanges) => {
|
||||
@@ -93,59 +279,160 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
}
|
||||
};
|
||||
|
||||
const filteredEnvironments
|
||||
= environments?.filter((env) => env.name.toLowerCase().includes(searchText.toLowerCase())) || [];
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
||||
{openImportModal && <ImportEnvironmentModal type="collection" collection={collection} onClose={() => setOpenImportModal(false)} />}
|
||||
{openManageSecretsModal && <ManageSecrets onClose={() => setOpenManageSecretsModal(false)} />}
|
||||
{openImportModal && (
|
||||
<ImportEnvironmentModal type="collection" collection={collection} onClose={() => setOpenImportModal(false)} />
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
<div>
|
||||
{switchEnvConfirmClose && (
|
||||
<div className="flex items-center justify-between tab-container px-1">
|
||||
<ConfirmSwitchEnv onCancel={() => handleConfirmSwitch(false)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="environments-sidebar flex flex-col">
|
||||
{environments
|
||||
&& environments.length
|
||||
&& environments.map((env) => (
|
||||
<ToolHint key={env.uid} text={env.name} toolhintId={env.uid} place="right">
|
||||
<div
|
||||
id={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
|
||||
>
|
||||
<span className="break-all">{env.name}</span>
|
||||
</div>
|
||||
</ToolHint>
|
||||
))}
|
||||
<div className="btn-create-environment" onClick={() => handleCreateEnvClick()}>
|
||||
+ <span>Create</span>
|
||||
</div>
|
||||
<div className="environments-container">
|
||||
{switchEnvConfirmClose && (
|
||||
<div className="confirm-switch-overlay">
|
||||
<ConfirmSwitchEnv onCancel={() => handleConfirmSwitch(false)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-auto btn-import-environment">
|
||||
<div className="flex items-center" onClick={() => handleImportClick()}>
|
||||
<IconDownload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Import</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2" onClick={() => setShowExportModal(true)}>
|
||||
<IconUpload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Export</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2" onClick={() => handleSecretsClick()}>
|
||||
<IconShieldLock size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Managing Secrets</span>
|
||||
</div>
|
||||
<div className="sidebar">
|
||||
<div className="sidebar-header">
|
||||
<h2 className="title">Environments</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="btn-action" onClick={() => handleCreateEnvClick()} title="Create environment">
|
||||
<IconPlus size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
<button className="btn-action" onClick={() => handleImportClick()} title="Import environment">
|
||||
<IconDownload size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
<button className="btn-action" onClick={() => handleExportClick()} title="Export environment">
|
||||
<IconUpload size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="search-container">
|
||||
<IconSearch size={14} strokeWidth={1.5} className="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search environments..."
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="environments-list">
|
||||
{filteredEnvironments.map((env) => (
|
||||
<div
|
||||
key={env.uid}
|
||||
id={env.uid}
|
||||
className={`environment-item ${selectedEnvironment.uid === env.uid ? 'active' : ''} ${renamingEnvUid === env.uid ? 'renaming' : ''} ${activeEnvironmentUid === env.uid ? 'activated' : ''}`}
|
||||
onClick={() => renamingEnvUid !== env.uid && handleEnvironmentClick(env)}
|
||||
onDoubleClick={() => handleEnvironmentDoubleClick(env)}
|
||||
>
|
||||
{renamingEnvUid === env.uid ? (
|
||||
<div className="rename-container" ref={renameContainerRef}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="environment-name-input"
|
||||
value={newEnvName}
|
||||
onChange={handleEnvNameChange}
|
||||
onKeyDown={handleEnvNameKeyDown}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div className="inline-actions">
|
||||
<button
|
||||
className="inline-action-btn save"
|
||||
onClick={handleSaveRename}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title="Save"
|
||||
>
|
||||
<IconCheck size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
className="inline-action-btn cancel"
|
||||
onClick={handleCancelRename}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title="Cancel"
|
||||
>
|
||||
<IconX size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="environment-name">{env.name}</span>
|
||||
<div className="environment-actions">
|
||||
{activeEnvironmentUid === env.uid ? (
|
||||
<div className="activated-checkmark" title="Active environment">
|
||||
<IconCheck size={16} strokeWidth={2} />
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="activate-btn"
|
||||
onClick={(e) => handleActivateEnvironment(e, env)}
|
||||
title="Activate environment"
|
||||
>
|
||||
<IconCheck size={16} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isCreatingInline && (
|
||||
<div className="environment-item creating" ref={createContainerRef}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="environment-name-input"
|
||||
value={newEnvName}
|
||||
onChange={handleEnvNameChange}
|
||||
onKeyDown={handleEnvNameKeyDown}
|
||||
placeholder="Environment name..."
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div className="inline-actions">
|
||||
<button
|
||||
className="inline-action-btn save"
|
||||
onClick={handleSaveNewEnv}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title="Save"
|
||||
>
|
||||
<IconCheck size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
className="inline-action-btn cancel"
|
||||
onClick={handleCancelCreate}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title="Cancel"
|
||||
>
|
||||
<IconX size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{envNameError && (isCreatingInline || renamingEnvUid) && <div className="env-error">{envNameError}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EnvironmentDetails
|
||||
environment={selectedEnvironment}
|
||||
collection={collection}
|
||||
setIsModified={setIsModified}
|
||||
originalEnvironmentVariables={originalEnvironmentVariables}
|
||||
onClose={onClose}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ManageSecrets = ({ onClose }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title="Manage Secrets" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
<p>In any collection, there are secrets that need to be managed.</p>
|
||||
<p className="mt-2">These secrets can be anything such as API keys, passwords, or tokens.</p>
|
||||
<p className="mt-4">Bruno offers three approaches to manage secrets in collections.</p>
|
||||
<p className="mt-2">
|
||||
Read more about it in our{' '}
|
||||
<a
|
||||
href="https://docs.usebruno.com/secrets-management/overview"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-link hover:underline"
|
||||
>
|
||||
docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageSecrets;
|
||||
@@ -1,89 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal/index';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: environment.name
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function (value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
if (values.name === environment.name) {
|
||||
return;
|
||||
}
|
||||
dispatch(renameEnvironment(values.name, environment.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment renamed successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while renaming the environment'));
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => {
|
||||
formik.handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="Rename Environment"
|
||||
confirmText="Rename"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-medium">
|
||||
Environment Name
|
||||
</label>
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RenameEnvironment;
|
||||
@@ -1,11 +1,51 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
button.btn-create-environment {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
svg {
|
||||
opacity: 0.3;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.shared-button {
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
transition: all 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
background: ${(props) => props.theme.listItem.hoverBg};
|
||||
border-color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,87 +1,64 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import React, { useState } from 'react';
|
||||
import CreateEnvironment from './CreateEnvironment';
|
||||
import CreateEnvironment from 'components/Environments/EnvironmentSettings/CreateEnvironment';
|
||||
import EnvironmentList from './EnvironmentList';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import { IconFileAlert } from '@tabler/icons';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import ExportEnvironmentModal from 'components/Environments/Common/ExportEnvironmentModal';
|
||||
|
||||
export const SharedButton = ({ children, className, onClick }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`rounded bg-transparent px-2.5 py-2 w-fit text-xs font-medium text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
|
||||
${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultTab = ({ setTab }) => {
|
||||
return (
|
||||
<div className="text-center items-center flex flex-col">
|
||||
<IconFileAlert size={64} strokeWidth={1} />
|
||||
<span className="font-medium mt-2">No environments found</span>
|
||||
<span className="font-extralight mt-2 text-zinc-500 dark:text-zinc-400">
|
||||
Get started by using the following buttons :
|
||||
</span>
|
||||
<div className="flex items-center justify-center mt-6">
|
||||
<SharedButton onClick={() => setTab('create')}>
|
||||
<span>Create Environment</span>
|
||||
</SharedButton>
|
||||
|
||||
<span className="mx-4">Or</span>
|
||||
|
||||
<SharedButton onClick={() => setTab('import')}>
|
||||
<span>Import Environment</span>
|
||||
</SharedButton>
|
||||
</div>
|
||||
const DefaultTab = ({ setTab }) => (
|
||||
<div className="empty-state">
|
||||
<IconFileAlert size={48} strokeWidth={1.5} />
|
||||
<div className="title">No Environments</div>
|
||||
<div className="actions">
|
||||
<button className="shared-button" onClick={() => setTab('create')}>
|
||||
Create Environment
|
||||
</button>
|
||||
<button className="shared-button" onClick={() => setTab('import')}>
|
||||
Import Environment
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
||||
const EnvironmentSettings = ({ collection, onClose }) => {
|
||||
const EnvironmentSettings = ({ collection }) => {
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
const { environments } = collection;
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
const [tab, setTab] = useState('default');
|
||||
const [showExportModal, setShowExportModal] = useState(false);
|
||||
|
||||
const environments = collection?.environments || [];
|
||||
|
||||
if (!environments || !environments.length) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="Environments" handleCancel={onClose} hideCancel={true} hideFooter={true}>
|
||||
{tab === 'create' ? (
|
||||
<CreateEnvironment collection={collection} onClose={() => setTab('default')} />
|
||||
) : tab === 'import' ? (
|
||||
<ImportEnvironmentModal type="collection" collection={collection} onClose={() => setTab('default')} />
|
||||
) : (
|
||||
<DefaultTab setTab={setTab} />
|
||||
)}
|
||||
</Modal>
|
||||
{tab === 'create' ? (
|
||||
<CreateEnvironment collection={collection} onClose={() => setTab('default')} />
|
||||
) : tab === 'import' ? (
|
||||
<ImportEnvironmentModal type="collection" collection={collection} onClose={() => setTab('default')} />
|
||||
) : (
|
||||
<DefaultTab setTab={setTab} />
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="lg" title="Environments" handleCancel={onClose} hideFooter={true}>
|
||||
<EnvironmentList
|
||||
selectedEnvironment={selectedEnvironment}
|
||||
setSelectedEnvironment={setSelectedEnvironment}
|
||||
collection={collection}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
onClose={onClose}
|
||||
setShowExportModal={setShowExportModal}
|
||||
/>
|
||||
</Modal>
|
||||
<EnvironmentList
|
||||
environments={environments}
|
||||
activeEnvironmentUid={collection?.activeEnvironmentUid}
|
||||
selectedEnvironment={selectedEnvironment}
|
||||
setSelectedEnvironment={setSelectedEnvironment}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
collection={collection}
|
||||
setShowExportModal={setShowExportModal}
|
||||
/>
|
||||
{showExportModal && (
|
||||
<ExportEnvironmentModal
|
||||
onClose={() => setShowExportModal(false)}
|
||||
environments={collection.environments}
|
||||
environments={environments}
|
||||
environmentType="collection"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import WorkspaceEnvironments from 'components/WorkspaceHome/WorkspaceEnvironments';
|
||||
|
||||
const GlobalEnvironmentSettings = () => {
|
||||
return <WorkspaceEnvironments />;
|
||||
};
|
||||
|
||||
export default GlobalEnvironmentSettings;
|
||||
@@ -1,78 +0,0 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import Portal from 'components/Portal/index';
|
||||
import { useFormik } from 'formik';
|
||||
import { copyGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const CopyEnvironment = ({ environment, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: environment.name + ' - Copy'
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(copyGlobalEnvironment({ name: values.name, environmentUid: environment.uid }))
|
||||
.then(() => {
|
||||
toast.success('Global environment created!');
|
||||
onClose();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error('An error occurred while created the environment');
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => {
|
||||
formik.handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title="Copy Global Environment" confirmText="Copy" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-medium">
|
||||
New Environment Name
|
||||
</label>
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyEnvironment;
|
||||
@@ -1,100 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const CreateEnvironment = ({ onClose, onEnvironmentCreated }) => {
|
||||
const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);
|
||||
|
||||
const validateEnvironmentName = (name) => {
|
||||
const trimmedName = name?.toLowerCase().trim();
|
||||
return globalEnvs.every((env) => env?.name?.toLowerCase().trim() !== trimmedName);
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'Must be at least 1 character')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function (value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('Name is required')
|
||||
.test('duplicate-name', 'Global Environment already exists', validateEnvironmentName)
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(addGlobalEnvironment({ name: values.name }))
|
||||
.then(() => {
|
||||
toast.success('Global environment created!');
|
||||
onClose();
|
||||
// Call the callback if provided
|
||||
if (onEnvironmentCreated) {
|
||||
onEnvironmentCreated();
|
||||
}
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while creating the environment'));
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => {
|
||||
formik.handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="Create Global Environment"
|
||||
confirmText="Create"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-medium">
|
||||
Environment Name
|
||||
</label>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
</div>
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateEnvironment;
|
||||
@@ -1,15 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
button.submit {
|
||||
color: white;
|
||||
background-color: var(--color-background-danger) !important;
|
||||
border: inherit !important;
|
||||
|
||||
&:hover {
|
||||
border: inherit !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { deleteGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
|
||||
const DeleteEnvironment = ({ onClose, environment }) => {
|
||||
const dispatch = useDispatch();
|
||||
const onConfirm = () => {
|
||||
dispatch(deleteGlobalEnvironment({ environmentUid: environment.uid }))
|
||||
.then(() => {
|
||||
toast.success('Global Environment deleted successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while deleting the environment'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<StyledWrapper>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="Delete Global Environment"
|
||||
confirmText="Delete"
|
||||
handleConfirm={onConfirm}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
Are you sure you want to delete <span className="font-medium">{environment.name}</span> ?
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteEnvironment;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
const ConfirmSwitchEnv = ({ onCancel }) => {
|
||||
return createPortal(
|
||||
<Modal
|
||||
size="md"
|
||||
title="Unsaved changes"
|
||||
confirmText="Save and Close"
|
||||
cancelText="Close without saving"
|
||||
disableEscapeKey={true}
|
||||
disableCloseOnOutsideClick={true}
|
||||
closeModalFadeTimeout={150}
|
||||
handleCancel={onCancel}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
hideFooter={true}
|
||||
>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-medium">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">You have unsaved changes in this environment.</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCancel}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</Modal>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmSwitchEnv;
|
||||
@@ -1,67 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 500;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
|
||||
padding: 4px 10px;
|
||||
vertical-align: middle;
|
||||
|
||||
&:nth-child(1),
|
||||
&:nth-child(4) {
|
||||
width: 70px;
|
||||
}
|
||||
&:nth-child(5) {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
user-select: none;
|
||||
}
|
||||
thead td {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-param {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
|
||||
.tooltip-mod {
|
||||
font-size: ${(props) => props.theme.font.size.xs} !important;
|
||||
width: 150px !important;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: transparent;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,224 +0,0 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
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 } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
|
||||
const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentVariables, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const addButtonRef = useRef(null);
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
|
||||
let _collection = cloneDeep(collection);
|
||||
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: environment.variables || [],
|
||||
validationSchema: Yup.array().of(
|
||||
Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
name: Yup.string()
|
||||
.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()
|
||||
})
|
||||
),
|
||||
onSubmit: (values) => {
|
||||
if (!formik.dirty) {
|
||||
toast.error('Nothing to save');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(saveGlobalEnvironment({ environmentUid: environment.uid, variables: cloneDeep(values) }))
|
||||
.then(() => {
|
||||
toast.success('Changes saved successfully');
|
||||
formik.resetForm({ values });
|
||||
setIsModified(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.error('An error occurred while saving the changes');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Effect to track modifications.
|
||||
React.useEffect(() => {
|
||||
setIsModified(formik.dirty);
|
||||
}, [formik.dirty]);
|
||||
|
||||
const ErrorMessage = ({ name }) => {
|
||||
const meta = formik.getFieldMeta(name);
|
||||
const id = uuid();
|
||||
if (!meta.error || !meta.touched) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
<IconAlertCircle id={id} className="text-red-600 cursor-pointer" size={20} />
|
||||
<Tooltip className="tooltip-mod" anchorId={id} html={meta.error || ''} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const addVariable = () => {
|
||||
const newVariable = {
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
};
|
||||
formik.setFieldValue(formik.values.length, newVariable, false);
|
||||
};
|
||||
|
||||
const handleRemoveVar = (id) => {
|
||||
formik.setValues(formik.values.filter((variable) => variable.uid !== id));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (formik.dirty) {
|
||||
// Smooth scrolling to the changed parameter is temporarily disabled
|
||||
// due to UX issues when editing the first row in a long list of environment variables.
|
||||
// addButtonRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [formik.values, formik.dirty]);
|
||||
|
||||
const handleReset = () => {
|
||||
formik.resetForm({ originalEnvironmentVariables });
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-6 mb-6">
|
||||
<div className="h-[50vh] overflow-y-auto w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td className="text-center">Enabled</td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td className="text-center">Secret</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formik.values.map((variable, index) => (
|
||||
<tr key={variable.uid} data-testid={`env-var-row-${variable.name}`}>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center" data-testid={`env-var-name-${index}`}>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative" data-testid={`env-var-value-${index}`}>
|
||||
<MultiLineEditor
|
||||
theme={storedTheme}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
isSecret={variable.secret}
|
||||
readOnly={typeof variable.value !== 'string'}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
enableBrunoVarInfo={false}
|
||||
/>
|
||||
</div>
|
||||
{typeof variable.value !== 'string' && (
|
||||
<span className="ml-2 flex items-center">
|
||||
<IconInfoCircle
|
||||
id={`${variable.name}-disabled-info-icon`}
|
||||
className="text-muted"
|
||||
size={16}
|
||||
/>
|
||||
<Tooltip
|
||||
anchorId={`${variable.name}-disabled-info-icon`}
|
||||
content="Non-string values set via scripts are read-only and can only be updated through scripts."
|
||||
place="top"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button
|
||||
ref={addButtonRef}
|
||||
className="btn-add-param text-link pr-2 py-3 mt-2 select-none"
|
||||
onClick={addVariable}
|
||||
data-testid="add-variable"
|
||||
>
|
||||
+ Add Variable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary mt-2" onClick={formik.handleSubmit} data-testid="save-env">
|
||||
Save
|
||||
</button>
|
||||
<button type="submit" className="ml-2 px-1 submit btn btn-md btn-secondary mt-2" onClick={handleReset} data-testid="reset-env">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default EnvironmentVariables;
|
||||
@@ -1,58 +0,0 @@
|
||||
import { IconCopy, IconDatabase, IconEdit, IconTrash } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
import CopyEnvironment from '../../CopyEnvironment';
|
||||
import DeleteEnvironment from '../../DeleteEnvironment';
|
||||
import RenameEnvironment from '../../RenameEnvironment';
|
||||
import EnvironmentVariables from './EnvironmentVariables';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
|
||||
const EnvironmentDetails = ({ environment, setIsModified, collection, allEnvironments }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
const [openCopyModal, setOpenCopyModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
|
||||
{openEditModal && (
|
||||
<RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} />
|
||||
)}
|
||||
{openDeleteModal && (
|
||||
<DeleteEnvironment
|
||||
onClose={() => setOpenDeleteModal(false)}
|
||||
environment={environment}
|
||||
/>
|
||||
)}
|
||||
{openCopyModal && (
|
||||
<CopyEnvironment onClose={() => setOpenCopyModal(false)} environment={environment} />
|
||||
)}
|
||||
<div className="flex">
|
||||
<div className="flex flex-grow items-center">
|
||||
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
<span className="ml-1 font-medium break-all">{environment.name}</span>
|
||||
</div>
|
||||
<div className="flex gap-x-2 pl-2">
|
||||
<ToolHint text="Edit Environment" toolhintId={`edit-${environment.uid}`}>
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
|
||||
</ToolHint>
|
||||
<ToolHint text="Copy Environment" toolhintId={`copy-${environment.uid}`}>
|
||||
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
|
||||
</ToolHint>
|
||||
<ToolHint text="Delete Environment" toolhintId={`delete-${environment.uid}`}>
|
||||
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} data-testid="delete-environment-button" />
|
||||
</ToolHint>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<EnvironmentVariables
|
||||
environment={environment}
|
||||
setIsModified={setIsModified}
|
||||
collection={collection}
|
||||
allEnvironments={allEnvironments}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentDetails;
|
||||
@@ -1,62 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
margin-inline: -1rem;
|
||||
margin-block: -1.5rem;
|
||||
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.bg};
|
||||
|
||||
.environments-sidebar {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
|
||||
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
||||
min-height: 400px;
|
||||
height: 100%;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.environment-item {
|
||||
min-width: 150px;
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
border-left: solid 2px transparent;
|
||||
text-decoration: none;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
|
||||
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-create-environment,
|
||||
.btn-import-environment {
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
color: ${(props) => props.theme.textLink};
|
||||
|
||||
span:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-import-environment {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,163 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import usePrevious from 'hooks/usePrevious';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment';
|
||||
import { IconDownload, IconShieldLock, IconUpload } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ConfirmSwitchEnv from './ConfirmSwitchEnv';
|
||||
import ManageSecrets from 'components/Environments/EnvironmentSettings/ManageSecrets/index';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import { isEqual } from 'lodash';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
|
||||
const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironment, setSelectedEnvironment, isModified, setIsModified, collection, setShowExportModal }) => {
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openImportModal, setOpenImportModal] = useState(false);
|
||||
const [openManageSecretsModal, setOpenManageSecretsModal] = useState(false);
|
||||
|
||||
const [switchEnvConfirmClose, setSwitchEnvConfirmClose] = useState(false);
|
||||
const [originalEnvironmentVariables, setOriginalEnvironmentVariables] = useState([]);
|
||||
|
||||
const envUids = environments ? environments.map((env) => env.uid) : [];
|
||||
const prevEnvUids = usePrevious(envUids);
|
||||
|
||||
useEffect(() => {
|
||||
if (!environments?.length) {
|
||||
setSelectedEnvironment(null);
|
||||
setOriginalEnvironmentVariables([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedEnvironment) {
|
||||
const _selectedEnvironment = environments?.find((env) => env?.uid === selectedEnvironment?.uid);
|
||||
const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);
|
||||
if (hasSelectedEnvironmentChanged) {
|
||||
setSelectedEnvironment(_selectedEnvironment);
|
||||
}
|
||||
setOriginalEnvironmentVariables(selectedEnvironment.variables);
|
||||
return;
|
||||
}
|
||||
|
||||
const environment = environments?.find((env) => env.uid === activeEnvironmentUid) || environments?.[0] || null;
|
||||
|
||||
setSelectedEnvironment(environment);
|
||||
setOriginalEnvironmentVariables(environment?.variables || []);
|
||||
}, [environments, activeEnvironmentUid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) {
|
||||
const newEnv = environments.find((env) => !prevEnvUids.includes(env.uid));
|
||||
if (newEnv) {
|
||||
setSelectedEnvironment(newEnv);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevEnvUids && prevEnvUids.length && envUids.length < prevEnvUids.length) {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}
|
||||
}, [envUids, environments, prevEnvUids]);
|
||||
|
||||
const handleEnvironmentClick = (env) => {
|
||||
if (!isModified) {
|
||||
setSelectedEnvironment(env);
|
||||
} else {
|
||||
setSwitchEnvConfirmClose(true);
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedEnvironment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleCreateEnvClick = () => {
|
||||
if (!isModified) {
|
||||
setOpenCreateModal(true);
|
||||
} else {
|
||||
setSwitchEnvConfirmClose(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportClick = () => {
|
||||
if (!isModified) {
|
||||
setOpenImportModal(true);
|
||||
} else {
|
||||
setSwitchEnvConfirmClose(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecretsClick = () => {
|
||||
setOpenManageSecretsModal(true);
|
||||
};
|
||||
|
||||
const handleExportClick = () => {
|
||||
if (setShowExportModal) {
|
||||
setShowExportModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSwitch = (saveChanges) => {
|
||||
if (!saveChanges) {
|
||||
setSwitchEnvConfirmClose(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{openCreateModal && <CreateEnvironment onClose={() => setOpenCreateModal(false)} />}
|
||||
{openImportModal && <ImportEnvironmentModal type="global" onClose={() => setOpenImportModal(false)} />}
|
||||
{openManageSecretsModal && <ManageSecrets onClose={() => setOpenManageSecretsModal(false)} />}
|
||||
|
||||
<div className="flex">
|
||||
<div>
|
||||
{switchEnvConfirmClose && (
|
||||
<div className="flex items-center justify-between tab-container px-1">
|
||||
<ConfirmSwitchEnv onCancel={() => handleConfirmSwitch(false)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="environments-sidebar flex flex-col">
|
||||
{environments
|
||||
&& environments.length
|
||||
&& environments.map((env) => (
|
||||
<ToolHint key={env.uid} text={env.name} toolhintId={env.uid} place="right">
|
||||
<div
|
||||
id={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle click
|
||||
>
|
||||
<span className="break-all">{env.name}</span>
|
||||
</div>
|
||||
</ToolHint>
|
||||
))}
|
||||
<div className="btn-create-environment" onClick={() => handleCreateEnvClick()}>
|
||||
+ <span>Create</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto btn-import-environment">
|
||||
<div className="flex items-center" onClick={() => handleImportClick()}>
|
||||
<IconDownload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Import</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2" onClick={() => handleExportClick()}>
|
||||
<IconUpload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Export</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2" onClick={() => handleSecretsClick()}>
|
||||
<IconShieldLock size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Managing Secrets</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EnvironmentDetails
|
||||
environment={selectedEnvironment}
|
||||
setIsModified={setIsModified}
|
||||
originalEnvironmentVariables={originalEnvironmentVariables}
|
||||
collection={collection}
|
||||
allEnvironments={environments}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentList;
|
||||
@@ -1,92 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal/index';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { renameGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const RenameEnvironment = ({ onClose, environment }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: environment.name
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function (value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
if (values.name === environment.name) {
|
||||
return;
|
||||
}
|
||||
dispatch(renameGlobalEnvironment({ name: values.name, environmentUid: environment.uid }))
|
||||
.then(() => {
|
||||
toast.success('Environment renamed successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error('An error occurred while renaming the environment');
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => {
|
||||
formik.handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="Rename Environment"
|
||||
confirmText="Rename"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-medium">
|
||||
Environment Name
|
||||
</label>
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RenameEnvironment;
|
||||
@@ -1,13 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
button.btn-create-environment {
|
||||
&:hover {
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,90 +0,0 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import React, { useState } from 'react';
|
||||
import CreateEnvironment from './CreateEnvironment';
|
||||
import EnvironmentList from './EnvironmentList';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconFileAlert } from '@tabler/icons';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import ExportEnvironmentModal from 'components/Environments/Common/ExportEnvironmentModal';
|
||||
|
||||
export const SharedButton = ({ children, className, onClick }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`rounded bg-transparent px-2.5 py-2 w-fit text-xs font-medium text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
|
||||
${className}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultTab = ({ setTab }) => {
|
||||
return (
|
||||
<div className="text-center items-center flex flex-col">
|
||||
<IconFileAlert size={64} strokeWidth={1} />
|
||||
<span className="font-medium mt-2">No Global Environments found</span>
|
||||
<div className="flex items-center justify-center mt-6">
|
||||
<SharedButton onClick={() => setTab('create')}>
|
||||
<span>Create Global Environment</span>
|
||||
</SharedButton>
|
||||
|
||||
<span className="mx-4">Or</span>
|
||||
|
||||
<SharedButton onClick={() => setTab('import')}>
|
||||
<span>Import Environment</span>
|
||||
</SharedButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EnvironmentSettings = ({ globalEnvironments, collection, activeGlobalEnvironmentUid, onClose }) => {
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
const environments = globalEnvironments;
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
const [tab, setTab] = useState('default');
|
||||
const [showExportModal, setShowExportModal] = useState(false);
|
||||
if (!environments || !environments.length) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title="Global Environments" handleCancel={onClose} hideCancel={true} hideFooter={true}>
|
||||
{tab === 'create' ? (
|
||||
<CreateEnvironment onClose={() => setTab('default')} />
|
||||
) : tab === 'import' ? (
|
||||
<ImportEnvironmentModal type="global" onClose={() => setTab('default')} />
|
||||
) : (
|
||||
<DefaultTab setTab={setTab} />
|
||||
)}
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="lg" title="Global Environments" handleCancel={onClose} hideFooter={true}>
|
||||
<EnvironmentList
|
||||
environments={globalEnvironments}
|
||||
activeEnvironmentUid={activeGlobalEnvironmentUid}
|
||||
selectedEnvironment={selectedEnvironment}
|
||||
setSelectedEnvironment={setSelectedEnvironment}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
collection={collection}
|
||||
setShowExportModal={setShowExportModal}
|
||||
/>
|
||||
</Modal>
|
||||
{showExportModal && (
|
||||
<ExportEnvironmentModal
|
||||
onClose={() => setShowExportModal(false)}
|
||||
environments={globalEnvironments}
|
||||
environmentType="global"
|
||||
/>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentSettings;
|
||||
@@ -34,6 +34,8 @@ import WSResponsePane from 'components/ResponsePane/WsResponsePane';
|
||||
import { useTabPaneBoundaries } from 'hooks/useTabPaneBoundaries/index';
|
||||
import ResponseExample from 'components/ResponseExample';
|
||||
import WorkspaceHome from 'components/WorkspaceHome';
|
||||
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
||||
import GlobalEnvironmentSettings from 'components/Environments/GlobalEnvironmentSettings';
|
||||
|
||||
const MIN_LEFT_PANE_WIDTH = 300;
|
||||
const MIN_RIGHT_PANE_WIDTH = 480;
|
||||
@@ -147,8 +149,6 @@ const RequestTabPanel = () => {
|
||||
};
|
||||
}, [handleMouseUp, handleMouseMove]);
|
||||
|
||||
// When devtools opens in vertical layout, reduce request pane height to ensure response pane is visible
|
||||
// When devtools closes, restore the previous height
|
||||
useEffect(() => {
|
||||
if (!isVerticalLayout) return;
|
||||
|
||||
@@ -171,11 +171,15 @@ const RequestTabPanel = () => {
|
||||
}
|
||||
}, [isConsoleOpen, isVerticalLayout]);
|
||||
|
||||
if (!activeTabUid) {
|
||||
if (!activeTabUid || !focusedTab) {
|
||||
return <WorkspaceHome />;
|
||||
}
|
||||
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.collectionUid) {
|
||||
if (focusedTab.type === 'global-environment-settings') {
|
||||
return <GlobalEnvironmentSettings />;
|
||||
}
|
||||
|
||||
if (!focusedTab.uid || !focusedTab.collectionUid) {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
@@ -226,6 +230,10 @@ const RequestTabPanel = () => {
|
||||
return <SecuritySettings collection={collection} />;
|
||||
}
|
||||
|
||||
if (focusedTab.type === 'environment-settings') {
|
||||
return <EnvironmentSettings collection={collection} />;
|
||||
}
|
||||
|
||||
if (!item || !item.uid) {
|
||||
return <RequestNotFound itemUid={activeTabUid} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import GradientCloseButton from './GradientCloseButton';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock, IconDatabase, IconWorld } from '@tabler/icons';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDraft }) => {
|
||||
const getTabInfo = (type, tabName) => {
|
||||
@@ -53,6 +53,22 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDra
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'environment-settings': {
|
||||
return (
|
||||
<>
|
||||
<IconDatabase size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">Environments</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'global-environment-settings': {
|
||||
return (
|
||||
<>
|
||||
<IconWorld size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">Global Environments</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import React, { useCallback, useState, useRef, Fragment, useMemo, useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import { saveRequest, saveCollectionRoot, saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { deleteRequestDraft, deleteCollectionDraft, deleteFolderDraft } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, saveCollectionRoot, saveFolderRoot, saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { deleteRequestDraft, deleteCollectionDraft, deleteFolderDraft, clearEnvironmentsDraft } from 'providers/ReduxStore/slices/collections';
|
||||
import { clearGlobalEnvironmentDraft } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import darkTheme from 'themes/dark';
|
||||
import lightTheme from 'themes/light';
|
||||
import { findItemInCollection, hasRequestChanges } from 'utils/collections';
|
||||
import ConfirmRequestClose from './ConfirmRequestClose';
|
||||
import ConfirmCollectionClose from './ConfirmCollectionClose';
|
||||
import ConfirmFolderClose from './ConfirmFolderClose';
|
||||
import ConfirmCloseEnvironment from 'components/Environments/ConfirmCloseEnvironment';
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
import SpecialTab from './SpecialTab';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -21,6 +24,7 @@ import GradientCloseButton from './GradientCloseButton';
|
||||
import { flattenItems } from 'utils/collections/index';
|
||||
import { closeWsConnection } from 'utils/network/index';
|
||||
import ExampleTab from '../ExampleTab';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid, hasOverflow, setHasOverflow }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -31,6 +35,8 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
const [showConfirmCollectionClose, setShowConfirmCollectionClose] = useState(false);
|
||||
const [showConfirmFolderClose, setShowConfirmFolderClose] = useState(false);
|
||||
const [showConfirmEnvironmentClose, setShowConfirmEnvironmentClose] = useState(false);
|
||||
const [showConfirmGlobalEnvironmentClose, setShowConfirmGlobalEnvironmentClose] = useState(false);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
@@ -152,8 +158,31 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
|
||||
const hasDraft = tab.type === 'collection-settings' && collection?.draft;
|
||||
const hasFolderDraft = tab.type === 'folder-settings' && folder?.draft;
|
||||
const hasEnvironmentDraft = tab.type === 'environment-settings' && collection?.environmentsDraft;
|
||||
const globalEnvironmentDraft = useSelector((state) => state.globalEnvironments.globalEnvironmentDraft);
|
||||
const hasGlobalEnvironmentDraft = tab.type === 'global-environment-settings' && globalEnvironmentDraft;
|
||||
|
||||
if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
|
||||
const handleCloseEnvironmentSettings = (event) => {
|
||||
if (!collection?.environmentsDraft) {
|
||||
return handleCloseClick(event);
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setShowConfirmEnvironmentClose(true);
|
||||
};
|
||||
|
||||
const handleCloseGlobalEnvironmentSettings = (event) => {
|
||||
if (!globalEnvironmentDraft) {
|
||||
return handleCloseClick(event);
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setShowConfirmGlobalEnvironmentClose(true);
|
||||
};
|
||||
|
||||
if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings', 'environment-settings', 'global-environment-settings'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className={`flex items-center justify-between tab-container px-2 ${tab.preview ? 'italic' : ''}`}
|
||||
@@ -214,12 +243,70 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showConfirmEnvironmentClose && tab.type === 'environment-settings' && (
|
||||
<ConfirmCloseEnvironment
|
||||
isGlobal={false}
|
||||
onCancel={() => setShowConfirmEnvironmentClose(false)}
|
||||
onCloseWithoutSave={() => {
|
||||
dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));
|
||||
dispatch(closeTabs({ tabUids: [tab.uid] }));
|
||||
setShowConfirmEnvironmentClose(false);
|
||||
}}
|
||||
onSaveAndClose={() => {
|
||||
const draft = collection.environmentsDraft;
|
||||
if (draft?.environmentUid && draft?.variables) {
|
||||
dispatch(saveEnvironment(draft.variables, draft.environmentUid, collection.uid))
|
||||
.then(() => {
|
||||
dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));
|
||||
dispatch(closeTabs({ tabUids: [tab.uid] }));
|
||||
setShowConfirmEnvironmentClose(false);
|
||||
toast.success('Environment saved');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err);
|
||||
toast.error('Failed to save environment');
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showConfirmGlobalEnvironmentClose && tab.type === 'global-environment-settings' && (
|
||||
<ConfirmCloseEnvironment
|
||||
isGlobal={true}
|
||||
onCancel={() => setShowConfirmGlobalEnvironmentClose(false)}
|
||||
onCloseWithoutSave={() => {
|
||||
dispatch(clearGlobalEnvironmentDraft());
|
||||
dispatch(closeTabs({ tabUids: [tab.uid] }));
|
||||
setShowConfirmGlobalEnvironmentClose(false);
|
||||
}}
|
||||
onSaveAndClose={() => {
|
||||
const draft = globalEnvironmentDraft;
|
||||
if (draft?.environmentUid && draft?.variables) {
|
||||
dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid: draft.environmentUid }))
|
||||
.then(() => {
|
||||
dispatch(clearGlobalEnvironmentDraft());
|
||||
dispatch(closeTabs({ tabUids: [tab.uid] }));
|
||||
setShowConfirmGlobalEnvironmentClose(false);
|
||||
toast.success('Global environment saved');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err);
|
||||
toast.error('Failed to save global environment');
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{tab.type === 'folder-settings' && !folder ? (
|
||||
<RequestTabNotFound handleCloseClick={handleCloseClick} />
|
||||
) : tab.type === 'folder-settings' ? (
|
||||
<SpecialTab handleCloseClick={handleCloseFolderSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} hasDraft={hasFolderDraft} />
|
||||
) : tab.type === 'collection-settings' ? (
|
||||
<SpecialTab handleCloseClick={handleCloseCollectionSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={collection?.name} hasDraft={hasDraft} />
|
||||
) : tab.type === 'environment-settings' ? (
|
||||
<SpecialTab handleCloseClick={handleCloseEnvironmentSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} hasDraft={hasEnvironmentDraft} />
|
||||
) : tab.type === 'global-environment-settings' ? (
|
||||
<SpecialTab handleCloseClick={handleCloseGlobalEnvironmentSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} hasDraft={hasGlobalEnvironmentDraft} />
|
||||
) : (
|
||||
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />
|
||||
)}
|
||||
|
||||
@@ -83,10 +83,6 @@ const RequestTabs = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!activeTab) {
|
||||
return <StyledWrapper>Something went wrong!</StyledWrapper>;
|
||||
}
|
||||
|
||||
const effectiveSidebarWidth = sidebarCollapsed ? 0 : leftSidebarWidth;
|
||||
const maxTablistWidth = screenWidth - effectiveSidebarWidth - 150;
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@ const CreateEnvironment = ({ onClose, onEnvironmentCreated }) => {
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('Name is required')
|
||||
.test('duplicate-name', 'Environment already exists', validateEnvironmentName)
|
||||
.test('duplicate-name', 'Global environment already exists', validateEnvironmentName)
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(addGlobalEnvironment({ name: values.name }))
|
||||
.then(() => {
|
||||
toast.success('Environment created!');
|
||||
toast.success('Global environment created!');
|
||||
onClose();
|
||||
// Call the callback if provided
|
||||
if (onEnvironmentCreated) {
|
||||
@@ -62,7 +62,7 @@ const CreateEnvironment = ({ onClose, onEnvironmentCreated }) => {
|
||||
<Portal>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="Create Environment"
|
||||
title="Create Global Environment"
|
||||
confirmText="Create"
|
||||
handleConfirm={onSubmit}
|
||||
handleCancel={onClose}
|
||||
|
||||
@@ -139,7 +139,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding: 12px 0;
|
||||
padding: 12px 2px;
|
||||
background: ${(props) => props.theme.bg};
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
@@ -10,14 +10,26 @@ import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import {
|
||||
saveGlobalEnvironment,
|
||||
setGlobalEnvironmentDraft,
|
||||
clearGlobalEnvironmentDraft
|
||||
} from 'providers/ReduxStore/slices/global-environments';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
|
||||
const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentVariables, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid, globalEnvironmentDraft } = useSelector(
|
||||
(state) => state.globalEnvironments
|
||||
);
|
||||
|
||||
const hasDraftForThisEnv = globalEnvironmentDraft?.environmentUid === environment.uid;
|
||||
|
||||
// Track environment changes for draft restoration
|
||||
const prevEnvUidRef = React.useRef(null);
|
||||
const mountedRef = React.useRef(false);
|
||||
|
||||
let _collection = collection ? cloneDeep(collection) : {};
|
||||
|
||||
@@ -26,6 +38,8 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
_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 [
|
||||
@@ -67,12 +81,10 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
const isLastRow = index === values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
|
||||
// Skip validation for the last empty row
|
||||
if (isLastRow && isEmptyRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate name for non-empty rows
|
||||
if (!variable.name || variable.name.trim() === '') {
|
||||
if (!errors[index]) errors[index] = {};
|
||||
errors[index].name = 'Name cannot be empty';
|
||||
@@ -86,20 +98,61 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
onSubmit: () => {}
|
||||
});
|
||||
|
||||
// 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 hasActualChanges = JSON.stringify(currentValues) !== JSON.stringify(savedValues);
|
||||
const currentValuesJson = JSON.stringify(currentValues);
|
||||
const savedValuesJson = JSON.stringify(savedValues);
|
||||
const hasActualChanges = currentValuesJson !== savedValuesJson;
|
||||
|
||||
setIsModified(hasActualChanges);
|
||||
}, [formik.values, environment.variables, setIsModified]);
|
||||
|
||||
// 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({
|
||||
environmentUid: environment.uid,
|
||||
variables: currentValues
|
||||
}));
|
||||
}
|
||||
} else if (hasDraftForThisEnv) {
|
||||
dispatch(clearGlobalEnvironmentDraft());
|
||||
}
|
||||
}, [formik.values, environment.variables, environment.uid, setIsModified, dispatch, hasDraftForThisEnv, globalEnvironmentDraft?.variables]);
|
||||
|
||||
const ErrorMessage = ({ name, index }) => {
|
||||
const meta = formik.getFieldMeta(name);
|
||||
const id = `error-${name}-${index}`;
|
||||
|
||||
// Don't show error for the last empty row
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
const variable = formik.values[index];
|
||||
const isEmptyRow = !variable?.name || variable.name.trim() === '';
|
||||
@@ -119,39 +172,47 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveVar = (id) => {
|
||||
const filteredValues = formik.values.filter((variable) => variable.uid !== id);
|
||||
const handleRemoveVar = useCallback((id) => {
|
||||
const currentValues = formik.values;
|
||||
|
||||
const lastRow = formik.values[formik.values.length - 1];
|
||||
const isLastEmptyRow = lastRow.uid === id && (!lastRow.name || lastRow.name.trim() === '');
|
||||
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() === '');
|
||||
|
||||
if (!hasEmptyLastRow) {
|
||||
filteredValues.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
const newValues = hasEmptyLastRow
|
||||
? filteredValues
|
||||
: [
|
||||
...filteredValues,
|
||||
{
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
|
||||
formik.setValues(filteredValues);
|
||||
};
|
||||
formik.setValues(newValues);
|
||||
}, [formik.values]);
|
||||
|
||||
const handleNameChange = (index, e) => {
|
||||
formik.handleChange(e);
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
|
||||
// If typing in the last row, add a new empty row
|
||||
if (isLastRow) {
|
||||
const newVariable = {
|
||||
uid: uuid(),
|
||||
@@ -161,15 +222,32 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
secret: false,
|
||||
enabled: true
|
||||
};
|
||||
// Use setTimeout to ensure the change is processed first
|
||||
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() === '') {
|
||||
@@ -223,8 +301,24 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
}
|
||||
];
|
||||
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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="table-container">
|
||||
@@ -271,6 +365,8 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
value={variable.name}
|
||||
placeholder={isLastEmptyRow ? 'Name' : ''}
|
||||
onChange={(e) => handleNameChange(index, e)}
|
||||
onBlur={() => handleNameBlur(index)}
|
||||
onKeyDown={(e) => handleNameKeyDown(index, e)}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} index={index} />
|
||||
</div>
|
||||
@@ -286,6 +382,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
isSecret={variable.secret}
|
||||
readOnly={typeof variable.value !== 'string'}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
{typeof variable.value !== 'string' && (
|
||||
@@ -341,4 +438,5 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentVariables;
|
||||
|
||||
@@ -81,7 +81,7 @@ const StyledWrapper = styled.div`
|
||||
.title-error {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
left: 20px;
|
||||
margin-top: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
|
||||
@@ -3,6 +3,7 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
position: relative;
|
||||
|
||||
@@ -10,6 +11,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.confirm-switch-overlay {
|
||||
|
||||
@@ -4,6 +4,7 @@ const StyledWrapper = styled.div`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
|
||||
.empty-state {
|
||||
|
||||
@@ -145,7 +145,7 @@ const WorkspaceHome = () => {
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: 'Overview' },
|
||||
{ id: 'environments', label: 'Environments' }
|
||||
{ id: 'environments', label: 'Global Environments' }
|
||||
];
|
||||
|
||||
const renderTabContent = () => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import toast from 'react-hot-toast';
|
||||
import find from 'lodash/find';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import GlobalSearchModal from 'components/GlobalSearchModal';
|
||||
@@ -15,7 +14,7 @@ import {
|
||||
saveCollectionSettings
|
||||
} from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findCollectionByUid, findItemInCollection } from 'utils/collections';
|
||||
import { closeTabs, reorderTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { addTab, closeTabs, reorderTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { getKeyBindingsForActionAllOS } from './keyMappings';
|
||||
|
||||
@@ -26,9 +25,6 @@ export const HotkeysProvider = (props) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isEnvironmentSettingsModalOpen = useSelector((state) => state.app.isEnvironmentSettingsModalOpen);
|
||||
const isGlobalEnvironmentSettingsModalOpen = useSelector((state) => state.app.isGlobalEnvironmentSettingsModalOpen);
|
||||
const [showEnvSettingsModal, setShowEnvSettingsModal] = useState(false);
|
||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||
const [showGlobalSearchModal, setShowGlobalSearchModal] = useState(false);
|
||||
|
||||
@@ -44,23 +40,24 @@ export const HotkeysProvider = (props) => {
|
||||
// save hotkey
|
||||
useEffect(() => {
|
||||
Mousetrap.bind([...getKeyBindingsForActionAllOS('save')], (e) => {
|
||||
if (isEnvironmentSettingsModalOpen || isGlobalEnvironmentSettingsModalOpen) {
|
||||
console.log('todo: save environment settings');
|
||||
} else {
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (activeTab) {
|
||||
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, activeTab.uid);
|
||||
if (item && item.uid) {
|
||||
if (activeTab.type === 'folder-settings') {
|
||||
dispatch(saveFolderRoot(collection.uid, item.uid));
|
||||
} else {
|
||||
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
|
||||
}
|
||||
} else if (activeTab.type === 'collection-settings') {
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (activeTab) {
|
||||
if (activeTab.type === 'environment-settings' || activeTab.type === 'global-environment-settings') {
|
||||
window.dispatchEvent(new CustomEvent('environment-save'));
|
||||
return false;
|
||||
}
|
||||
|
||||
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, activeTab.uid);
|
||||
if (item && item.uid) {
|
||||
if (activeTab.type === 'folder-settings') {
|
||||
dispatch(saveFolderRoot(collection.uid, item.uid));
|
||||
} else {
|
||||
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
|
||||
}
|
||||
} else if (activeTab.type === 'collection-settings') {
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +68,7 @@ export const HotkeysProvider = (props) => {
|
||||
return () => {
|
||||
Mousetrap.unbind([...getKeyBindingsForActionAllOS('save')]);
|
||||
};
|
||||
}, [activeTabUid, tabs, saveRequest, collections, isEnvironmentSettingsModalOpen, isGlobalEnvironmentSettingsModalOpen]);
|
||||
}, [activeTabUid, tabs, saveRequest, collections, dispatch]);
|
||||
|
||||
// send request (ctrl/cmd + enter)
|
||||
useEffect(() => {
|
||||
@@ -120,7 +117,13 @@ export const HotkeysProvider = (props) => {
|
||||
const collection = findCollectionByUid(collections, activeTab.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
setShowEnvSettingsModal(true);
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'environment-settings'
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +133,7 @@ export const HotkeysProvider = (props) => {
|
||||
return () => {
|
||||
Mousetrap.unbind([...getKeyBindingsForActionAllOS('editEnvironment')]);
|
||||
};
|
||||
}, [activeTabUid, tabs, collections, setShowEnvSettingsModal]);
|
||||
}, [activeTabUid, tabs, collections, dispatch]);
|
||||
|
||||
// new request (ctrl/cmd + b)
|
||||
useEffect(() => {
|
||||
@@ -281,9 +284,6 @@ export const HotkeysProvider = (props) => {
|
||||
|
||||
return (
|
||||
<HotkeysContext.Provider {...props} value="hotkey">
|
||||
{showEnvSettingsModal && (
|
||||
<EnvironmentSettings collection={currentCollection} onClose={() => setShowEnvSettingsModal(false)} />
|
||||
)}
|
||||
{showNewRequestModal && (
|
||||
<NewRequest collectionUid={currentCollection?.uid} onClose={() => setShowNewRequestModal(false)} />
|
||||
)}
|
||||
|
||||
@@ -69,12 +69,6 @@ export const appSlice = createSlice({
|
||||
updateIsDragging: (state, action) => {
|
||||
state.isDragging = action.payload.isDragging;
|
||||
},
|
||||
updateEnvironmentSettingsModalVisibility: (state, action) => {
|
||||
state.isEnvironmentSettingsModalOpen = action.payload;
|
||||
},
|
||||
updateGlobalEnvironmentSettingsModalVisibility: (state, action) => {
|
||||
state.isGlobalEnvironmentSettingsModalOpen = action.payload;
|
||||
},
|
||||
showHomePage: (state) => {
|
||||
state.showHomePage = true;
|
||||
state.showApiSpecPage = false;
|
||||
@@ -141,8 +135,6 @@ export const {
|
||||
refreshScreenWidth,
|
||||
updateLeftSidebarWidth,
|
||||
updateIsDragging,
|
||||
updateEnvironmentSettingsModalVisibility,
|
||||
updateGlobalEnvironmentSettingsModalVisibility,
|
||||
showHomePage,
|
||||
hideHomePage,
|
||||
showManageWorkspacePage,
|
||||
|
||||
@@ -694,6 +694,19 @@ export const collectionsSlice = createSlice({
|
||||
folder.draft = null;
|
||||
}
|
||||
},
|
||||
setEnvironmentsDraft: (state, action) => {
|
||||
const { collectionUid, environmentUid, variables } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
if (collection) {
|
||||
collection.environmentsDraft = { environmentUid, variables };
|
||||
}
|
||||
},
|
||||
clearEnvironmentsDraft: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
if (collection) {
|
||||
collection.environmentsDraft = null;
|
||||
}
|
||||
},
|
||||
newEphemeralHttpRequest: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -3388,6 +3401,8 @@ export const {
|
||||
saveFolderDraft,
|
||||
deleteCollectionDraft,
|
||||
deleteFolderDraft,
|
||||
setEnvironmentsDraft,
|
||||
clearEnvironmentsDraft,
|
||||
newEphemeralHttpRequest,
|
||||
collapseFullCollection,
|
||||
toggleCollection,
|
||||
|
||||
@@ -5,7 +5,8 @@ import { cloneDeep, has } from 'lodash';
|
||||
|
||||
const initialState = {
|
||||
globalEnvironments: [],
|
||||
activeGlobalEnvironmentUid: null
|
||||
activeGlobalEnvironmentUid: null,
|
||||
globalEnvironmentDraft: null
|
||||
};
|
||||
|
||||
export const globalEnvironmentsSlice = createSlice({
|
||||
@@ -73,6 +74,13 @@ export const globalEnvironmentsSlice = createSlice({
|
||||
state.activeGlobalEnvironmentUid = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
setGlobalEnvironmentDraft: (state, action) => {
|
||||
const { environmentUid, variables } = action.payload;
|
||||
state.globalEnvironmentDraft = { environmentUid, variables };
|
||||
},
|
||||
clearGlobalEnvironmentDraft: (state) => {
|
||||
state.globalEnvironmentDraft = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -84,7 +92,9 @@ export const {
|
||||
_renameGlobalEnvironment,
|
||||
_copyGlobalEnvironment,
|
||||
_selectGlobalEnvironment,
|
||||
_deleteGlobalEnvironment
|
||||
_deleteGlobalEnvironment,
|
||||
setGlobalEnvironmentDraft,
|
||||
clearGlobalEnvironmentDraft
|
||||
} = globalEnvironmentsSlice.actions;
|
||||
|
||||
const getWorkspaceContext = (state) => {
|
||||
|
||||
@@ -24,7 +24,9 @@ export const tabsSlice = createSlice({
|
||||
const nonReplaceableTabTypes = [
|
||||
'variables',
|
||||
'collection-runner',
|
||||
'security-settings'
|
||||
'security-settings',
|
||||
'environment-settings',
|
||||
'global-environment-settings'
|
||||
];
|
||||
|
||||
const existingTab = find(state.tabs, (tab) => tab.uid === uid);
|
||||
|
||||
@@ -39,17 +39,17 @@ export const validateNameError = (name) => {
|
||||
}
|
||||
|
||||
if (!firstCharacter.test(name[0])) {
|
||||
return 'Invalid first character.';
|
||||
return `Special characters aren't allowed in the name. Invalid character '${name[0]}'.`;
|
||||
}
|
||||
|
||||
for (let i = 1; i < name.length - 1; i++) {
|
||||
if (!middleCharacters.test(name[i])) {
|
||||
return `Invalid character '${name[i]}' at position ${i + 1}.`;
|
||||
return `Special characters aren't allowed in the name. Invalid character '${name[i]}'.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!lastCharacter.test(name[name.length - 1])) {
|
||||
return 'Invalid last character.';
|
||||
return `Special characters aren't allowed in the name. Invalid character '${name[name.length - 1]}'.`;
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
@@ -28,9 +28,14 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
// open environment configuration
|
||||
await page.locator('#configure-env').click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
// we restart the app to confirm that the environment variable is persisted
|
||||
const newApp = await restartApp();
|
||||
@@ -43,11 +48,15 @@ test.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => {
|
||||
// open environment dropdown
|
||||
await newPage.getByTestId('environment-selector-trigger').click();
|
||||
await newPage.locator('#configure-env').click();
|
||||
|
||||
const newEnvTab = newPage.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(newEnvTab).toBeVisible();
|
||||
|
||||
await expect(newPage.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(newPage.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
|
||||
|
||||
// close the environment modal
|
||||
await newPage.getByTestId('modal-close-button').click();
|
||||
await newEnvTab.hover();
|
||||
await newEnvTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
// Restore the original Stage.bru file
|
||||
fs.writeFileSync(originalStageBruPath, originalStageBruContent);
|
||||
|
||||
@@ -21,9 +21,14 @@ test.describe.serial('bru.setEnvVar(name, value)', () => {
|
||||
// confirm that the environment variable is set
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.locator('#configure-env').click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
// we restart the app to confirm that the environment variable is not persisted
|
||||
const newApp = await restartApp();
|
||||
@@ -37,11 +42,13 @@ test.describe.serial('bru.setEnvVar(name, value)', () => {
|
||||
await newPage.getByTestId('environment-selector-trigger').click();
|
||||
await newPage.locator('#configure-env').click();
|
||||
|
||||
// ensure that the environment variable is not persisted
|
||||
await expect(newPage.locator('table.environment-variables tbody')).not.toContainText('token');
|
||||
const newEnvTab = newPage.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(newEnvTab).toBeVisible();
|
||||
|
||||
// close the environment variable modal
|
||||
await newPage.getByTestId('modal-close-button').click();
|
||||
await expect(newPage.locator('.table-container tbody')).not.toContainText('token');
|
||||
|
||||
await newEnvTab.hover();
|
||||
await newEnvTab.getByTestId('request-tab-close-icon').click();
|
||||
await newPage.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,18 +14,20 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => {
|
||||
await page.locator('#configure-env').click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Remove the test environment variables
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
|
||||
const key1Row = page.getByRole('row', { name: 'multiple-persist-vars-key1' });
|
||||
if (await key1Row.isVisible()) {
|
||||
await key1Row.getByRole('button').click(); // Click the delete button
|
||||
await key1Row.getByRole('button').click();
|
||||
}
|
||||
|
||||
const key2Row = page.getByRole('row', { name: 'multiple-persist-vars-key2' });
|
||||
if (await key2Row.isVisible()) {
|
||||
await key2Row.getByRole('button').click(); // Click the delete button
|
||||
await key2Row.getByRole('button').click();
|
||||
}
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors to avoid masking test failures
|
||||
@@ -74,11 +76,16 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => {
|
||||
await page.waitForTimeout(200);
|
||||
await page.locator('#configure-env').click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('row', { name: 'multiple-persist-vars-key1' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'value1' }).getByRole('cell').nth(2)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'multiple-persist-vars-key2' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'value2' }).getByRole('cell').nth(2)).toBeVisible();
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
|
||||
await test.step('Verify variables are persisted to file', async () => {
|
||||
|
||||
@@ -28,15 +28,13 @@ test.describe('Collection Environment Configuration Selection Tests', () => {
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
// Verify the config modal opens with the currently active environment selected
|
||||
const collectionEnvModal = page.locator('.bruno-modal').filter({ hasText: 'Environments' });
|
||||
await expect(collectionEnvModal).toBeVisible();
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
// Check that the active environment in the config matches prod
|
||||
const activeEnvItem = collectionEnvModal.locator('.environment-item.active');
|
||||
const activeEnvItem = page.locator('.environment-item.active');
|
||||
await expect(activeEnvItem).toContainText('prod');
|
||||
|
||||
// Close the collection environment config modal
|
||||
await page.getByText('×').click();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
createEnvironment,
|
||||
addEnvironmentVariables,
|
||||
saveEnvironment,
|
||||
closeEnvironmentPanel,
|
||||
sendRequest,
|
||||
expectResponseContains,
|
||||
removeCollection
|
||||
@@ -39,7 +38,6 @@ test.describe('Collection Environment Create Tests', () => {
|
||||
]);
|
||||
|
||||
await saveEnvironment(page);
|
||||
await closeEnvironmentPanel(page);
|
||||
await expect(locators.environment.currentEnvironment()).toContainText('Test Environment');
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
createEnvironment,
|
||||
addEnvironmentVariables,
|
||||
saveEnvironment,
|
||||
closeEnvironmentPanel,
|
||||
sendRequest,
|
||||
expectResponseContains,
|
||||
closeAllCollections
|
||||
@@ -41,7 +40,6 @@ test.describe('Global Environment Create Tests', () => {
|
||||
]);
|
||||
|
||||
await saveEnvironment(page);
|
||||
await closeEnvironmentPanel(page);
|
||||
await expect(locators.environment.currentEnvironment()).toContainText('Test Global Environment');
|
||||
});
|
||||
|
||||
|
||||
@@ -36,14 +36,13 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
// Verify the environment settings modal opens
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Open export modal and configure export settings', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.locator('button[title="Export environment"]').click();
|
||||
|
||||
// Verify export modal opens
|
||||
const exportModal = page.locator('.bruno-modal').filter({ hasText: 'Export Environments' });
|
||||
@@ -63,8 +62,6 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export the environment
|
||||
await page.getByRole('button', { name: 'Export 1 Environment' }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported file and content', async () => {
|
||||
@@ -95,11 +92,14 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for multiple environments', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.locator('button[title="Export environment"]').click();
|
||||
|
||||
// Verify all environments are selected by default
|
||||
await expect(page.getByRole('checkbox', { name: 'Local' })).toBeChecked();
|
||||
@@ -115,8 +115,6 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export all environments
|
||||
await page.getByRole('button', { name: /Export \d+ Environments?/ }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported files and content', async () => {
|
||||
@@ -162,11 +160,14 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings with folder format', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.locator('button[title="Export environment"]').click();
|
||||
|
||||
// Select folder export format (default might be single JSON file)
|
||||
await page.getByText('Separate files in folder').click();
|
||||
@@ -178,8 +179,6 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export should succeed with unique names
|
||||
await page.getByRole('button', { name: 'Export 2 Environment' }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify unique naming and file content', async () => {
|
||||
@@ -219,11 +218,13 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for single JSON file', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.locator('button[title="Export environment"]').click();
|
||||
|
||||
// Deselect all environments first
|
||||
await page.getByText('Deselect All').click();
|
||||
@@ -244,8 +245,6 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
|
||||
// Verify success message
|
||||
await expect(page.getByText('Environment(s) exported successfully', { exact: false }).first()).toBeVisible();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported file and content', async () => {
|
||||
@@ -274,11 +273,13 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for single JSON file', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.locator('button[title="Export environment"]').click();
|
||||
|
||||
// Select single JSON file format
|
||||
await page.getByText('Single JSON file').click();
|
||||
@@ -293,8 +294,6 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
|
||||
// Verify success message
|
||||
await expect(page.getByText('Environment(s) exported successfully', { exact: false }).first()).toBeVisible();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported file and content', async () => {
|
||||
@@ -329,11 +328,13 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for single JSON file', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.locator('button[title="Export environment"]').click();
|
||||
|
||||
// Deselect all environments first
|
||||
await page.getByText('Deselect All').click();
|
||||
@@ -351,8 +352,6 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export should succeed with unique names
|
||||
await page.getByRole('button', { name: 'Export 1 Environment' }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify unique naming and file content', async () => {
|
||||
@@ -387,11 +386,14 @@ test.describe.serial('Collection Environment Export Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-collection').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Open export modal and deselect all environments', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.getByRole('button', { name: 'Export Environment' }).click();
|
||||
|
||||
// Deselect all environments
|
||||
await page.getByText('Deselect All').click();
|
||||
|
||||
@@ -40,14 +40,13 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
// Verify the global environment settings modal opens
|
||||
const globalEnvModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
|
||||
await expect(globalEnvModal).toBeVisible();
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Open export modal and configure export settings', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.locator('button[title="Export environment"]').click();
|
||||
|
||||
// Verify export modal opens
|
||||
const exportModal = page.locator('.bruno-modal').filter({ hasText: 'Export Environments' });
|
||||
@@ -67,8 +66,6 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export the environment
|
||||
await page.getByRole('button', { name: 'Export 1 Environment' }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported file and content', async () => {
|
||||
@@ -99,11 +96,14 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for multiple environments', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.locator('button[title="Export environment"]').click();
|
||||
|
||||
// Verify all environments are selected by default
|
||||
await expect(page.getByRole('checkbox', { name: 'Local' })).toBeChecked();
|
||||
@@ -119,8 +119,6 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export all environments
|
||||
await page.getByRole('button', { name: /Export \d+ Environments?/ }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported files and content', async () => {
|
||||
@@ -166,11 +164,14 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings with folder format', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.locator('button[title="Export environment"]').click();
|
||||
|
||||
// Set export directory
|
||||
await page.locator('input[id="export-location"]').fill(exportDir);
|
||||
@@ -182,8 +183,6 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export should succeed with unique names
|
||||
await page.getByRole('button', { name: 'Export 2 Environment' }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').first().click();
|
||||
});
|
||||
|
||||
await test.step('Verify unique naming and file content', async () => {
|
||||
@@ -223,11 +222,13 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for single JSON file', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.locator('button[title="Export environment"]').click();
|
||||
|
||||
// Deselect all environments first
|
||||
await page.getByText('Deselect All').click();
|
||||
@@ -250,8 +251,6 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
|
||||
// Verify success message
|
||||
await expect(page.getByText('Environment(s) exported successfully', { exact: false }).first()).toBeVisible();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported file and content', async () => {
|
||||
@@ -280,11 +279,13 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for single JSON file', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.locator('button[title="Export environment"]').click();
|
||||
|
||||
// Select single JSON file format
|
||||
await page.getByText('Single JSON file').click();
|
||||
@@ -299,8 +300,6 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await page.waitForTimeout(200);
|
||||
// Verify success message
|
||||
await expect(page.getByText('Environment(s) exported successfully', { exact: false }).first()).toBeVisible();
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
});
|
||||
|
||||
await test.step('Verify exported file and content', async () => {
|
||||
@@ -335,11 +334,13 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Configure export settings for single JSON file', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.locator('button[title="Export environment"]').click();
|
||||
|
||||
// Deselect all environments first
|
||||
await page.getByText('Deselect All').click();
|
||||
@@ -359,8 +360,6 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await test.step('Execute export and close modal', async () => {
|
||||
// Export should succeed with unique names
|
||||
await page.getByRole('button', { name: 'Export 1 Environment' }).click();
|
||||
|
||||
await page.getByTestId('modal-close-button').first().click();
|
||||
});
|
||||
|
||||
await test.step('Verify unique naming and file content', async () => {
|
||||
@@ -395,18 +394,18 @@ test.describe.serial('Global Environment Export Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Open export modal and deselect all environments', async () => {
|
||||
// Click export button
|
||||
await page.locator('.btn-import-environment').getByText('Export').click();
|
||||
await page.getByRole('button', { name: 'Export Environment' }).click();
|
||||
|
||||
// Deselect all environments
|
||||
await page.getByText('Deselect All').click();
|
||||
});
|
||||
|
||||
await test.step('Verify export button is disabled when no environments selected', async () => {
|
||||
// Verify export button is disabled
|
||||
await expect(page.getByRole('button', { name: 'Export Environments' })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,15 +21,13 @@ test.describe('Global Environment Configuration Selection Tests', () => {
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
// Verify the config modal opens with the currently active environment selected
|
||||
const globalEnvModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
|
||||
await expect(globalEnvModal).toBeVisible();
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
// Check that the active environment in the config matches the current environment
|
||||
const activeEnvItem = globalEnvModal.locator('.environment-item.active');
|
||||
const activeEnvItem = page.locator('.environment-item.active');
|
||||
await expect(activeEnvItem).toContainText(currentEnvName);
|
||||
|
||||
// Close the global environment config modal and go to welcome page
|
||||
await page.getByText('×').click();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,16 +39,14 @@ test.describe.serial('Collection Environment Import Tests', () => {
|
||||
});
|
||||
|
||||
await test.step('Verify imported environment and variables', async () => {
|
||||
// The environment settings modal should now be visible with the imported environment
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
// Verify imported variables
|
||||
await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
// Close modal
|
||||
await page.getByText('×').click();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
|
||||
await test.step('Clean up after test', async () => {
|
||||
@@ -93,15 +91,11 @@ test.describe.serial('Collection Environment Import Tests', () => {
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(multiEnvFile);
|
||||
|
||||
// The environment settings modal should now be visible with the imported environments
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify both environments are available in selector', async () => {
|
||||
// Check that both environments are available in the selector
|
||||
await page.getByText('×').click(); // Close environment settings modal
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
|
||||
@@ -118,15 +112,16 @@ test.describe.serial('Collection Environment Import Tests', () => {
|
||||
// Verify prod environment variables by opening settings again
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
// Verify prod environment variables
|
||||
await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
// Close modal
|
||||
await page.getByText('×').click();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
|
||||
await test.step('Clean up after test', async () => {
|
||||
|
||||
@@ -23,12 +23,10 @@ test.describe.serial('Global Environment Import Tests', () => {
|
||||
|
||||
// Delete all existing environments
|
||||
for (let i = 0; i < count; i++) {
|
||||
await page.getByTestId('delete-environment-button').click();
|
||||
// Confirm deletion if there's a confirmation dialog
|
||||
await page.locator('button[title="Delete"]').first().click();
|
||||
const confirmButton = page.getByRole('button', { name: 'Delete' });
|
||||
if (await confirmButton.isVisible()) {
|
||||
await confirmButton.click();
|
||||
await page.getByText('×').click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,16 +54,15 @@ test.describe.serial('Global Environment Import Tests', () => {
|
||||
});
|
||||
|
||||
await test.step('Verify imported global environment and variables', async () => {
|
||||
// The global environment settings modal should now be visible with the imported environment
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
// Verify imported variables
|
||||
await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
// Close modal
|
||||
await page.getByText('×').click();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,12 +87,10 @@ test.describe.serial('Global Environment Import Tests', () => {
|
||||
|
||||
// Delete all existing environments
|
||||
for (let i = 0; i < count; i++) {
|
||||
await page.getByTestId('delete-environment-button').click();
|
||||
// Confirm deletion if there's a confirmation dialog
|
||||
const confirmButton = page.getByRole('button', { name: 'Delete' });
|
||||
await page.locator('button[title="Delete"]').first().click();
|
||||
const confirmButton = page.getByText('Delete', { exact: true });
|
||||
if (await confirmButton.isVisible()) {
|
||||
await confirmButton.click();
|
||||
await page.getByText('×').click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,14 +115,11 @@ test.describe.serial('Global Environment Import Tests', () => {
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(multiEnvFile);
|
||||
|
||||
// The global environment settings modal should now be visible with the imported environments
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Verify both global environments are available in selector', async () => {
|
||||
// Check that both environments are available in the selector
|
||||
await page.getByText('×').click(); // Close environment settings modal
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
|
||||
@@ -145,15 +137,15 @@ test.describe.serial('Global Environment Import Tests', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
const envModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
|
||||
await expect(envModal).toBeVisible();
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
// Verify imported variables
|
||||
await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();
|
||||
|
||||
// Close modal
|
||||
await page.getByText('×').click();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,21 +52,19 @@ test.describe('Collection Environment Import Tests', () => {
|
||||
// Wait for import to complete and environment settings modal to open
|
||||
await expect(page.locator('.current-environment')).toContainText('Test Collection Environment');
|
||||
|
||||
// The environment settings modal should now be visible with the imported environment
|
||||
const envSettingsModal = page.locator('.bruno-modal').filter({ hasText: 'Environments' });
|
||||
await expect(envSettingsModal).toBeVisible();
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
// Verify imported variables in Test Collection Environment settings
|
||||
await expect(envSettingsModal.locator('input[name="0.name"]')).toHaveValue('host');
|
||||
await expect(envSettingsModal.locator('input[name="1.name"]')).toHaveValue('userId');
|
||||
await expect(envSettingsModal.locator('input[name="2.name"]')).toHaveValue('apiKey');
|
||||
await expect(envSettingsModal.locator('input[name="3.name"]')).toHaveValue('postTitle');
|
||||
await expect(envSettingsModal.locator('input[name="4.name"]')).toHaveValue('postBody');
|
||||
await expect(envSettingsModal.locator('input[name="5.name"]')).toHaveValue('secretApiToken');
|
||||
await expect(envSettingsModal.locator('input[name="5.secret"]')).toBeChecked();
|
||||
await page.getByText('×').click();
|
||||
await expect(page.locator('input[name="0.name"]')).toHaveValue('host');
|
||||
await expect(page.locator('input[name="1.name"]')).toHaveValue('userId');
|
||||
await expect(page.locator('input[name="2.name"]')).toHaveValue('apiKey');
|
||||
await expect(page.locator('input[name="3.name"]')).toHaveValue('postTitle');
|
||||
await expect(page.locator('input[name="4.name"]')).toHaveValue('postBody');
|
||||
await expect(page.locator('input[name="5.name"]')).toHaveValue('secretApiToken');
|
||||
await expect(page.locator('input[name="5.secret"]')).toBeChecked();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
// Test GET request with imported environment
|
||||
await page.locator('.collection-item-name').first().click();
|
||||
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}');
|
||||
await page.locator('[data-testid="send-arrow-icon"]').click();
|
||||
|
||||
@@ -47,21 +47,20 @@ test.describe('Global Environment Import Tests', () => {
|
||||
// Wait for import to complete and global environment settings modal to open
|
||||
await expect(page.locator('.current-environment')).toContainText('Test Global Environment');
|
||||
|
||||
// The global environment settings modal should now be visible with the imported environment
|
||||
const globalEnvSettingsModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
|
||||
await expect(globalEnvSettingsModal).toBeVisible();
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
// Verify imported variables in Test Global Environment settings
|
||||
await expect(globalEnvSettingsModal.locator('input[name="0.name"]')).toHaveValue('host');
|
||||
await expect(globalEnvSettingsModal.locator('input[name="1.name"]')).toHaveValue('userId');
|
||||
await expect(globalEnvSettingsModal.locator('input[name="2.name"]')).toHaveValue('apiKey');
|
||||
await expect(globalEnvSettingsModal.locator('input[name="3.name"]')).toHaveValue('postTitle');
|
||||
await expect(globalEnvSettingsModal.locator('input[name="4.name"]')).toHaveValue('postBody');
|
||||
await expect(globalEnvSettingsModal.locator('input[name="5.name"]')).toHaveValue('secretApiToken');
|
||||
await expect(globalEnvSettingsModal.locator('input[name="5.secret"]')).toBeChecked();
|
||||
await page.getByText('×').click();
|
||||
const variablesTable = page.locator('.table-container');
|
||||
await expect(variablesTable.locator('input[name="0.name"]')).toHaveValue('host');
|
||||
await expect(variablesTable.locator('input[name="1.name"]')).toHaveValue('userId');
|
||||
await expect(variablesTable.locator('input[name="2.name"]')).toHaveValue('apiKey');
|
||||
await expect(variablesTable.locator('input[name="3.name"]')).toHaveValue('postTitle');
|
||||
await expect(variablesTable.locator('input[name="4.name"]')).toHaveValue('postBody');
|
||||
await expect(variablesTable.locator('input[name="5.name"]')).toHaveValue('secretApiToken');
|
||||
await expect(variablesTable.locator('input[name="5.secret"]')).toBeChecked();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
// Test GET request with global environment
|
||||
await page.locator('#collection-environment-test-collection .collection-item-name').first().click();
|
||||
await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}');
|
||||
await page.locator('[data-testid="send-arrow-icon"]').click();
|
||||
|
||||
@@ -11,7 +11,7 @@ test.describe('Multiline Variables - Write Test', () => {
|
||||
|
||||
// open request
|
||||
await expect(page.getByTitle('multiline-test', { exact: true })).toBeVisible();
|
||||
await page.getByTitle('multiline-test', { exact: true }).click();
|
||||
await page.getByTitle('multiline-test', { exact: true }).dblclick();
|
||||
|
||||
// open environment dropdown
|
||||
await page.locator('div.current-environment').click();
|
||||
@@ -28,10 +28,12 @@ test.describe('Multiline Variables - Write Test', () => {
|
||||
await expect(page.getByText('Configure', { exact: true })).toBeVisible();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
// add variable
|
||||
await page.getByRole('button', { name: /Add.*Variable/i }).click();
|
||||
const valueTextarea = page.locator('.bruno-modal-card textarea').last();
|
||||
await expect(valueTextarea).toBeVisible();
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
const emptyRowNameInput = page.locator('tbody tr').last().locator('input[placeholder="Name"]');
|
||||
await expect(emptyRowNameInput).toBeVisible();
|
||||
await emptyRowNameInput.fill('multiline_data_json');
|
||||
|
||||
const jsonValue = `{
|
||||
"user": {
|
||||
@@ -48,23 +50,17 @@ test.describe('Multiline Variables - Write Test', () => {
|
||||
}
|
||||
}`;
|
||||
|
||||
// fill variable value
|
||||
await valueTextarea.fill(jsonValue);
|
||||
await page.keyboard.press('Shift+Tab');
|
||||
await page.keyboard.type('multiline_data_json');
|
||||
const variableRow = page.locator('tbody tr').filter({ has: page.locator('input[value="multiline_data_json"]') });
|
||||
const codeMirror = variableRow.locator('.CodeMirror');
|
||||
await codeMirror.click();
|
||||
await page.keyboard.insertText(jsonValue);
|
||||
|
||||
// save variable and close config
|
||||
const saveVarButton = page.getByRole('button', { name: /Save/i });
|
||||
await expect(saveVarButton).toBeVisible();
|
||||
await saveVarButton.click();
|
||||
await page.getByTestId('save-env').click();
|
||||
|
||||
await expect(page.locator('.close.cursor-pointer')).toBeVisible();
|
||||
await page.locator('.close.cursor-pointer').click();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
|
||||
// send request
|
||||
const sendButton = page.locator('#send-request').getByRole('img').nth(2);
|
||||
await expect(sendButton).toBeVisible();
|
||||
await sendButton.click();
|
||||
await page.getByTestId('send-arrow-icon').click();
|
||||
|
||||
// wait for response status
|
||||
await expect(page.locator('.response-status-code.text-ok')).toBeVisible();
|
||||
|
||||
@@ -3,7 +3,6 @@ import { closeAllCollections } from '../../utils/page';
|
||||
|
||||
test.describe('Global Environment Variable Update via Script', () => {
|
||||
test.afterEach(async ({ pageWithUserData: page }) => {
|
||||
// cleanup: close all collections
|
||||
await closeAllCollections(page);
|
||||
});
|
||||
|
||||
@@ -23,40 +22,43 @@ test.describe('Global Environment Variable Update via Script', () => {
|
||||
await page.getByTestId('send-arrow-icon').click();
|
||||
});
|
||||
|
||||
await test.step('Open the Global Environment Config modal', async () => {
|
||||
await test.step('Open the Global Environment Config tab', async () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
const globalEnvModal = page.locator('.bruno-modal').filter({ hasText: 'Global Environments' });
|
||||
|
||||
await test.step('Verify that the value of "existingEnvEnabled" is updated by the pre-request script', async () => {
|
||||
const updatedExistingEnvEnabledInputDiv = await globalEnvModal.getByTestId('env-var-value-1');
|
||||
const updatedExistingEnvEnabledValue = await updatedExistingEnvEnabledInputDiv.locator('.CodeMirror-line').textContent();
|
||||
await expect(updatedExistingEnvEnabledValue).toContain('newExistingEnvEnabledValue');
|
||||
const row = page.locator('tbody tr').filter({ has: page.locator('input[value="existingEnvEnabled"]') });
|
||||
const value = await row.locator('.CodeMirror-line').first().textContent();
|
||||
await expect(value).toContain('newExistingEnvEnabledValue');
|
||||
});
|
||||
|
||||
await test.step('Verify that the value of "existingEnvDisabled" is updated by the pre-request script', async () => {
|
||||
const updatedExistingEnvDisabledInputDiv = await globalEnvModal.getByTestId('env-var-value-2');
|
||||
const updatedExistingEnvDisabledValue = await updatedExistingEnvDisabledInputDiv.locator('.CodeMirror-line').textContent();
|
||||
await expect(updatedExistingEnvDisabledValue).toContain('newExistingEnvDisabledValue');
|
||||
const row = page.locator('tbody tr').filter({ has: page.locator('input[value="existingEnvDisabled"]') });
|
||||
const value = await row.locator('.CodeMirror-line').first().textContent();
|
||||
await expect(value).toContain('newExistingEnvDisabledValue');
|
||||
});
|
||||
|
||||
await test.step('Verify that a new env variable "newEnv" is added by the pre-request script to the global environment', async () => {
|
||||
const newEnvInputDiv = await globalEnvModal.getByTestId('env-var-value-3');
|
||||
const newEnvValue = await newEnvInputDiv.locator('.CodeMirror-line').textContent();
|
||||
await expect(newEnvValue).toContain('newEnvValue');
|
||||
const row = page.locator('tbody tr').filter({ has: page.locator('input[value="newEnv"]') });
|
||||
const value = await row.locator('.CodeMirror-line').first().textContent();
|
||||
await expect(value).toContain('newEnvValue');
|
||||
});
|
||||
|
||||
await test.step('Verify that the value of "baseUrl" is unchanged.', async () => {
|
||||
const currentBaseUrlInputDiv = await globalEnvModal.getByTestId('env-var-value-0');
|
||||
const currentBaseUrlValue = await currentBaseUrlInputDiv.locator('.CodeMirror-line').textContent();
|
||||
await expect(currentBaseUrlValue).toContain('https://echo.usebruno.com');
|
||||
const row = page.locator('tbody tr').filter({ has: page.locator('input[value="baseUrl"]') });
|
||||
const value = await row.locator('.CodeMirror-line').first().textContent();
|
||||
await expect(value).toContain('https://echo.usebruno.com');
|
||||
});
|
||||
|
||||
await test.step('Close the global environment config modal.', async () => {
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
await test.step('Close the global environment config tab.', async () => {
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
import { openCollectionAndAcceptSandbox, closeAllCollections, sendRequest } from '../utils/page';
|
||||
import { openCollectionAndAcceptSandbox, closeAllCollections, sendRequest, addEnvironmentVariables } from '../utils/page';
|
||||
import { buildCommonLocators } from '../utils/page/locators';
|
||||
|
||||
test.describe('Global Environment Variables - Non-string Values', () => {
|
||||
@@ -23,20 +23,19 @@ test.describe('Global Environment Variables - Non-string Values', () => {
|
||||
await page.locator('#environment-name').fill('Test Env');
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
// Add a string variable.
|
||||
await page.getByTestId('add-variable').click();
|
||||
const newRow = page.locator('tbody tr').last();
|
||||
await newRow.locator('input[name$=".name"]').fill('stringVar');
|
||||
await newRow.locator('.CodeMirror').click();
|
||||
await page.keyboard.type('hello world');
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
|
||||
await addEnvironmentVariables(page, [
|
||||
{ name: 'stringVar', value: 'hello world' },
|
||||
{ name: 'numericVar', value: '170001' },
|
||||
{ name: 'booleanVar', value: 'true' }
|
||||
]);
|
||||
|
||||
// Save
|
||||
await page.getByTestId('save-env').click();
|
||||
|
||||
// Verify that the string variable value is saved and displayed correctly.
|
||||
await expect(newRow.locator('.CodeMirror-line').first()).toContainText('hello world');
|
||||
// Close the environment modal
|
||||
await page.locator('[data-test-id="modal-close-button"]').click();
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
|
||||
// Request contains a script that sets the non-string global variables.
|
||||
@@ -50,14 +49,13 @@ test.describe('Global Environment Variables - Non-string Values', () => {
|
||||
await page.getByTestId('environment-selector-trigger').click();
|
||||
await page.getByTestId('env-tab-global').click();
|
||||
await page.getByRole('button', { name: 'Configure' }).click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await expect(envTab).toBeVisible();
|
||||
});
|
||||
|
||||
const envModal = page
|
||||
.locator('.bruno-modal-card')
|
||||
.filter({ has: page.locator('.bruno-modal-header-title', { hasText: 'Global Environments' }) });
|
||||
|
||||
const numericInput = envModal.locator('input[value="numericVar"]');
|
||||
const booleanInput = envModal.locator('input[value="booleanVar"]');
|
||||
const numericInput = page.locator('input[value="numericVar"]');
|
||||
const booleanInput = page.locator('input[value="booleanVar"]');
|
||||
await expect(numericInput).toBeVisible();
|
||||
await expect(booleanInput).toBeVisible();
|
||||
const numericRow = numericInput.locator('xpath=ancestor::tr');
|
||||
@@ -74,9 +72,7 @@ test.describe('Global Environment Variables - Non-string Values', () => {
|
||||
await page.keyboard.type('999');
|
||||
await expect(numericRow.locator('.CodeMirror-line').first()).toContainText(/170001/);
|
||||
|
||||
// Hovering over the info icon reveals the tooltip.
|
||||
// It is anchored to the info icon element id, so hover/click reveals it reliably.
|
||||
const infoIcon = page.locator('#numericVar-disabled-info-icon');
|
||||
const infoIcon = numericRow.locator('[id$="-disabled-info-icon"]').nth(0);
|
||||
await infoIcon.hover();
|
||||
|
||||
// The tooltip explains why the field is locked.
|
||||
@@ -103,8 +99,7 @@ test.describe('Global Environment Variables - Non-string Values', () => {
|
||||
await page.keyboard.type('false');
|
||||
await expect(booleanRow.locator('.CodeMirror-line').first()).toContainText(/true/);
|
||||
|
||||
// Hovering over the info icon reveals the tooltip.
|
||||
const infoIcon = page.locator('#booleanVar-disabled-info-icon');
|
||||
const infoIcon = booleanRow.locator('[id$="-disabled-info-icon"]').nth(0);
|
||||
await infoIcon.hover();
|
||||
|
||||
// The tooltip explains why the field is locked.
|
||||
@@ -114,8 +109,7 @@ test.describe('Global Environment Variables - Non-string Values', () => {
|
||||
});
|
||||
|
||||
await test.step('Verify that stringVar remains editable', async () => {
|
||||
// Unlike script-managed values above, this one is user-managed.
|
||||
const stringInput = envModal.locator('input[value="stringVar"]');
|
||||
const stringInput = page.locator('input[value="stringVar"]');
|
||||
await expect(stringInput).toBeVisible();
|
||||
const stringRow = stringInput.locator('xpath=ancestor::tr');
|
||||
|
||||
@@ -125,8 +119,12 @@ test.describe('Global Environment Variables - Non-string Values', () => {
|
||||
|
||||
// Verify the user edit persists in the UI.
|
||||
await expect(stringRow.locator('.CodeMirror-line').first()).toContainText('hello world updated');
|
||||
// Close the environment modal
|
||||
await page.locator('[data-test-id="modal-close-button"]').click();
|
||||
|
||||
await page.getByTestId('save-env').click();
|
||||
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,9 +178,10 @@ test.describe('Import Insomnia v4 Collection - Environment Import', () => {
|
||||
await expect(page.getByTestId('env-var-row-newFeature.version').locator('.CodeMirror-line').first()).toHaveText('2.099123123');
|
||||
});
|
||||
|
||||
await test.step('Close environment modal', async () => {
|
||||
// Close the environment configuration modal to ensure clean state
|
||||
await page.getByText('×').click();
|
||||
await test.step('Close environment tab', async () => {
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -202,9 +202,10 @@ test.describe('Import Insomnia v5 Collection - Environment Import', () => {
|
||||
await expect(page.getByTestId('env-var-row-user.roles[0]').locator('.CodeMirror-line').first()).toHaveText('admin');
|
||||
});
|
||||
|
||||
await test.step('Close environment modal', async () => {
|
||||
// Close the environment configuration modal to ensure clean state
|
||||
await page.getByText('×').click();
|
||||
await test.step('Close environment tab', async () => {
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ test('should persist request with newlines across app restarts', async ({ create
|
||||
await page.locator('.bruno-modal').getByLabel('Location').fill(collectionPath);
|
||||
await page.locator('.bruno-modal').getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
await openCollectionAndAcceptSandbox(page, 'newlines-persistence', 'safe');
|
||||
|
||||
const collection = page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'newlines-persistence' });
|
||||
await collection.hover();
|
||||
await collection.locator('.collection-actions .icon').click();
|
||||
@@ -25,7 +27,6 @@ test('should persist request with newlines across app restarts', async ({ create
|
||||
await page.locator('#new-request-url').locator('textarea').fill('https://httpbin.org/get');
|
||||
await page.locator('.bruno-modal').getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
await openCollectionAndAcceptSandbox(page, 'newlines-persistence', 'safe');
|
||||
await page.locator('.collection-item-name').filter({ hasText: 'persistence-test' }).dblclick();
|
||||
|
||||
await page.getByRole('tab', { name: 'Params' }).click();
|
||||
|
||||
@@ -390,10 +390,23 @@ const createEnvironment = async (
|
||||
|
||||
await page.locator('button[id="create-env"]').click();
|
||||
|
||||
const nameInput = page.locator('input[name="name"]');
|
||||
const nameInput = type === 'collection'
|
||||
? page.locator('input[name="name"]')
|
||||
: page.locator('#environment-name');
|
||||
await expect(nameInput).toBeVisible();
|
||||
await nameInput.fill(environmentName);
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
const tabLabel = type === 'collection' ? 'Environments' : 'Global Environments';
|
||||
await expect(page.locator('.request-tab').filter({ hasText: tabLabel })).toBeVisible();
|
||||
|
||||
const locators = buildCommonLocators(page);
|
||||
await locators.environment.selector().click();
|
||||
if (type === 'global') {
|
||||
await locators.environment.globalTab().click();
|
||||
}
|
||||
await locators.environment.envOption(environmentName).click();
|
||||
await expect(page.locator('.current-environment')).toContainText(environmentName);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -416,11 +429,6 @@ const addEnvironmentVariable = async (
|
||||
index: number
|
||||
) => {
|
||||
await test.step(`Add environment variable "${variable.name}"`, async () => {
|
||||
const addButton = page.locator('button[data-testid="add-variable"]');
|
||||
await addButton.waitFor({ state: 'visible' });
|
||||
await addButton.click();
|
||||
|
||||
// Wait for the new row to be added and the name input to be visible
|
||||
const nameInput = page.locator(`input[name="${index}.name"]`);
|
||||
await nameInput.waitFor({ state: 'visible' });
|
||||
await nameInput.fill(variable.name);
|
||||
@@ -466,13 +474,17 @@ const saveEnvironment = async (page: Page) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Close the environment modal/panel
|
||||
* Close the environment tab
|
||||
* @param page - The page object
|
||||
* @param type - The type of environment tab to close
|
||||
* @returns void
|
||||
*/
|
||||
const closeEnvironmentPanel = async (page: Page) => {
|
||||
await test.step('Close environment panel', async () => {
|
||||
await page.getByText('×').click();
|
||||
const closeEnvironmentPanel = async (page: Page, type: EnvironmentType = 'collection') => {
|
||||
await test.step('Close environment tab', async () => {
|
||||
const tabLabel = type === 'collection' ? 'Environments' : 'Global Environments';
|
||||
const envTab = page.locator('.request-tab').filter({ hasText: tabLabel });
|
||||
await envTab.hover();
|
||||
await envTab.getByTestId('request-tab-close-icon').click();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export const buildCommonLocators = (page: Page) => ({
|
||||
tabs: {
|
||||
requestTab: (requestName: string) => page.locator('.request-tab .tab-label').filter({ hasText: requestName }),
|
||||
activeRequestTab: () => page.locator('.request-tab.active'),
|
||||
closeTab: (requestName: string) => page.locator('.request-tab').filter({ hasText: requestName }).locator('.close-icon')
|
||||
closeTab: (requestName: string) => page.locator('.request-tab').filter({ hasText: requestName }).getByTestId('request-tab-close-icon')
|
||||
},
|
||||
folder: {
|
||||
chevron: (folderName: string) => page.locator('.collection-item-name').filter({ hasText: folderName }).getByTestId('folder-chevron')
|
||||
|
||||
Reference in New Issue
Block a user