diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/EnvironmentListContent/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/EnvironmentListContent/index.js new file mode 100644 index 000000000..5aa1cde1b --- /dev/null +++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/EnvironmentListContent/index.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { IconPlus, IconDownload, IconSettings } from '@tabler/icons'; + +const EnvironmentListContent = ({ + environments, + activeEnvironmentUid, + description, + onEnvironmentSelect, + onSettingsClick, + onCreateClick, + onImportClick +}) => { + return ( +
+ {environments && environments.length > 0 ? ( + <> +
+
onEnvironmentSelect(null)}> + No Environment +
+
+ {environments.map((env) => ( +
onEnvironmentSelect(env)} + > + {env.name} +
+ ))} +
+
+ +
+
+ + ) : ( +
+

Ready to get started?

+

{description}

+
+ + +
+
+ )} +
+ ); +}; + +export default EnvironmentListContent; diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js index b1c09d5f2..d2b3d2d7c 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js @@ -2,14 +2,204 @@ import styled from 'styled-components'; const Wrapper = styled.div` .current-environment { - background-color: ${(props) => props.theme.sidebar.badge.bg}; - border-radius: 15px; + border-radius: 0.9375rem; + padding: 0.25rem 0.5rem 0.25rem 0.75rem; + user-select: none; + background-color: transparent; + border: 1px solid ${(props) => props.theme.dropdown.selectedColor}; + line-height: 1rem; .caret { margin-left: 0.25rem; color: rgb(140, 140, 140); fill: rgb(140, 140, 140); } + + .env-icon { + margin-right: 0.25rem; + color: ${(props) => props.theme.dropdown.selectedColor}; + } + + .env-text { + color: ${(props) => props.theme.dropdown.selectedColor}; + font-size: 0.875rem; + } + + .env-separator { + color: #8c8c8c; + margin: 0 0.25rem; + opacity: 0.7; + } + + .env-text-inactive { + color: ${(props) => props.theme.dropdown.color}; + font-size: 0.875rem; + opacity: 0.7; + } + + &.no-environments { + background-color: ${(props) => props.theme.sidebar.badge.bg}; + border: 1px solid transparent; + color: ${(props) => props.theme.dropdown.secondaryText}; + } + } + + .tippy-box { + min-width: 11.875rem; + min-height: 15.0625rem; + font-size: 0.8125rem; + position: relative; + } + + .tippy-box .tippy-content { + padding: 0; + + .dropdown-item { + display: flex; + align-items: center; + padding: 0.35rem 0.6rem; + cursor: pointer; + font-size: 0.8125rem; + color: ${(props) => props.theme.dropdown.primaryText}; + + &:hover:not(:disabled) { + background-color: ${(props) => props.theme.dropdown.hoverBg}; + } + + &.active { + background-color: ${(props) => props.theme.dropdown.selectedBg}; + color: ${(props) => props.theme.dropdown.selectedColor}; + } + + &.no-environment { + color: ${(props) => props.theme.dropdown.mutedText}; + } + } + } + + .configure-button { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background-color: ${(props) => props.theme.dropdown.bg}; + border-top: 0.0625rem solid ${(props) => props.theme.dropdown.separator}; + z-index: 10; + margin: 0; + + button { + color: ${(props) => props.theme.dropdown.primaryText}; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + gap: 0.5rem; + } + } + + .tab-button { + color: var(--color-tab-inactive); + font-size: 0.8125rem; + + .tab-content-wrapper { + position: relative; + display: flex; + align-items: center; + gap: 0.125rem; + } + + &.active { + color: ${(props) => props.theme.tabs.active.color}; + border-bottom-color: ${(props) => props.theme.tabs.active.border}; + } + + &.inactive { + border-bottom-color: transparent; + } + } + + .dropdown-item-list { + max-height: 75vh; + overflow-y: scroll; + } + + .empty-state { + max-width: 20rem; + margin: 0 auto; + padding: 0.35rem 0.6rem; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 12.5rem; + + h3 { + color: ${(props) => props.theme.dropdown.primaryText}; + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.5rem; + line-height: 1.4; + } + + p { + color: ${(props) => props.theme.dropdown.primaryText}; + opacity: 0.75; + font-size: 0.6875rem; + line-height: 1.5; + margin-bottom: 1rem; + max-width: 11.875rem; + margin: 0 auto; + margin-bottom: 1rem; + } + + .space-y-2 { + width: 100%; + align-self: stretch; + } + + .space-y-2 > button { + border: 0.0625rem solid ${(props) => props.theme.dropdown.primaryText}; + background: transparent; + color: ${(props) => props.theme.dropdown.primaryText}; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + width: 100%; + margin-bottom: 0.5rem; + font-size: 0.75rem; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + + &:hover { + background-color: ${(props) => props.theme.dropdown.hoverBg}; + } + + &:last-child { + margin-bottom: 0; + } + } + } + + .no-collection-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem 1rem; + color: ${(props) => props.theme.dropdown.primaryText}; + font-size: 0.8125rem; + line-height: 1.5; + text-align: center; + opacity: 0.75; + + svg { + margin: 0 auto 1rem auto; + color: ${(props) => props.theme.dropdown.primaryText}; + opacity: 0.5; + } } `; diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js index 52053179f..e2be165e8 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js @@ -1,95 +1,240 @@ -import React, { useRef, forwardRef, useState } from 'react'; +import React, { useState, useRef, forwardRef } from 'react'; import find from 'lodash/find'; import Dropdown from 'components/Dropdown'; -import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions'; +import { IconWorld, IconDatabase, IconCaretDown, IconSettings, IconPlus, IconDownload } from '@tabler/icons'; +import { useSelector, useDispatch } from 'react-redux'; import { updateEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app'; -import { IconSettings, IconCaretDown, IconDatabase, IconDatabaseOff } from '@tabler/icons'; -import EnvironmentSettings from '../EnvironmentSettings'; +import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions'; +import { selectGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments'; import toast from 'react-hot-toast'; -import { useDispatch } from 'react-redux'; +import EnvironmentListContent from './EnvironmentListContent/index'; +import EnvironmentSettings from '../EnvironmentSettings'; +import GlobalEnvironmentSettings from 'components/GlobalEnvironments/EnvironmentSettings'; +import CreateEnvironment from '../EnvironmentSettings/CreateEnvironment'; +import ImportEnvironment from '../EnvironmentSettings/ImportEnvironment'; +import CreateGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment'; +import ImportGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment'; import StyledWrapper from './StyledWrapper'; const EnvironmentSelector = ({ collection }) => { const dispatch = useDispatch(); const dropdownTippyRef = useRef(); - const [openSettingsModal, setOpenSettingsModal] = useState(false); - const { environments, activeEnvironmentUid } = collection; - const activeEnvironment = activeEnvironmentUid ? find(environments, (e) => e.uid === activeEnvironmentUid) : null; + const [activeTab, setActiveTab] = useState('collection'); + const [showGlobalSettings, setShowGlobalSettings] = useState(false); + const [showCollectionSettings, setShowCollectionSettings] = useState(false); + const [showCreateGlobalModal, setShowCreateGlobalModal] = useState(false); + const [showImportGlobalModal, setShowImportGlobalModal] = useState(false); + const [showCreateCollectionModal, setShowCreateCollectionModal] = useState(false); + const [showImportCollectionModal, setShowImportCollectionModal] = useState(false); + const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments); + const activeGlobalEnvironmentUid = useSelector((state) => state.globalEnvironments.activeGlobalEnvironmentUid); + const activeGlobalEnvironment = activeGlobalEnvironmentUid + ? find(globalEnvironments, (e) => e.uid === activeGlobalEnvironmentUid) + : null; + + const environments = collection?.environments || []; + const activeEnvironmentUid = collection?.activeEnvironmentUid; + const activeCollectionEnvironment = activeEnvironmentUid + ? find(environments, (e) => e.uid === activeEnvironmentUid) + : null; + + const tabs = [ + { id: 'collection', label: 'Collection', icon: }, + { id: 'global', label: 'Global', icon: } + ]; + + const onDropdownCreate = (ref) => { + dropdownTippyRef.current = ref; + }; + + // Get description based on active tab + const description = + activeTab === 'collection' + ? 'Create your first environment to begin working with your collection.' + : 'Create your first global environment to begin working across collections.'; + + // Environment selection handler + const handleEnvironmentSelect = (environment) => { + const action = + activeTab === 'collection' + ? selectEnvironment(environment ? environment.uid : null, collection.uid) + : selectGlobalEnvironment({ environmentUid: environment ? environment.uid : null }); + + dispatch(action) + .then(() => { + if (environment) { + toast.success(`Environment changed to ${environment.name}`); + } else { + toast.success('No Environments are active now'); + } + dropdownTippyRef.current.hide(); + }) + .catch((err) => { + toast.error('An error occurred while selecting the environment'); + }); + }; + + // Settings handler + const handleSettingsClick = () => { + if (activeTab === 'collection') { + dispatch(updateEnvironmentSettingsModalVisibility(true)); + setShowCollectionSettings(true); + } else { + setShowGlobalSettings(true); + } + dropdownTippyRef.current.hide(); + }; + + // Create handler + const handleCreateClick = () => { + if (activeTab === 'collection') { + setShowCreateCollectionModal(true); + } else { + setShowCreateGlobalModal(true); + } + dropdownTippyRef.current.hide(); + }; + + // Import handler + const handleImportClick = () => { + if (activeTab === 'collection') { + setShowImportCollectionModal(true); + } else { + setShowImportGlobalModal(true); + } + dropdownTippyRef.current.hide(); + }; + + // Modal handlers + const handleCloseSettings = () => { + setShowGlobalSettings(false); + setShowCollectionSettings(false); + dispatch(updateEnvironmentSettingsModalVisibility(false)); + }; + + // Create icon component for dropdown trigger const Icon = forwardRef((props, ref) => { + const hasAnyEnv = activeGlobalEnvironment || activeCollectionEnvironment; + + const displayContent = hasAnyEnv ? ( + <> + {activeCollectionEnvironment && ( + <> +
+ + {activeCollectionEnvironment.name} +
+ {activeGlobalEnvironment && |} + + )} + {activeGlobalEnvironment && ( +
+ + {activeGlobalEnvironment.name} +
+ )} + + ) : ( + No environments + ); + return ( -
-

{activeEnvironment ? activeEnvironment.name : 'No Environment'}

+
+ {displayContent}
); }); - const handleSettingsIconClick = () => { - setOpenSettingsModal(true); - dispatch(updateEnvironmentSettingsModalVisibility(true)); - }; - - const handleModalClose = () => { - setOpenSettingsModal(false); - dispatch(updateEnvironmentSettingsModalVisibility(false)); - }; - - const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); - - const onSelect = (environment) => { - dispatch(selectEnvironment(environment ? environment.uid : null, collection.uid)) - .then(() => { - if (environment) { - toast.success(`Environment changed to ${environment.name}`); - } else { - toast.success(`No Environments are active now`); - } - }) - .catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment')); - }; - return ( -
+
} placement="bottom-end"> -
Collection Environments
- {environments && environments.length - ? environments.map((e) => ( -
{ - onSelect(e); - dropdownTippyRef.current.hide(); - }} - > - {e.name} -
- )) - : null} -
{ - dropdownTippyRef.current.hide(); - onSelect(null); - }} - > - - No Environment + {/* Tab Headers */} +
+ {tabs.map((tab) => ( + + ))}
-
{ - handleSettingsIconClick(); - dropdownTippyRef.current.hide(); - }}> -
- -
- Configure + + {/* Tab Content */} +
+
- {openSettingsModal && } + + {/* Modals - Rendered outside dropdown to avoid conflicts */} + {showGlobalSettings && ( + + )} + + {showCollectionSettings && } + + {showCreateGlobalModal && ( + setShowCreateGlobalModal(false)} + onEnvironmentCreated={() => { + setShowGlobalSettings(true); + }} + /> + )} + + {showImportGlobalModal && ( + setShowImportGlobalModal(false)} + onEnvironmentCreated={() => { + setShowGlobalSettings(true); + }} + /> + )} + + {showCreateCollectionModal && ( + setShowCreateCollectionModal(false)} + onEnvironmentCreated={() => { + setShowCollectionSettings(true); + }} + /> + )} + + {showImportCollectionModal && ( + setShowImportCollectionModal(false)} + onEnvironmentCreated={() => { + setShowCollectionSettings(true); + }} + /> + )} ); }; diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js index 4f3dcb5ba..3b86f599d 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js @@ -8,7 +8,7 @@ import Portal from 'components/Portal'; import Modal from 'components/Modal'; import { validateName, validateNameError } from 'utils/common/regex'; -const CreateEnvironment = ({ collection, onClose }) => { +const CreateEnvironment = ({ collection, onClose, onEnvironmentCreated }) => { const dispatch = useDispatch(); const inputRef = useRef(); @@ -37,6 +37,10 @@ const CreateEnvironment = ({ collection, onClose }) => { .then(() => { toast.success('Environment created in collection'); onClose(); + // Call the callback if provided + if (onEnvironmentCreated) { + onEnvironmentCreated(); + } }) .catch(() => toast.error('An error occurred while creating the environment')); } diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index ce2e13678..1328b2b9c 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -254,6 +254,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original 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 @@ -261,15 +262,15 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
- - - diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/ImportEnvironment/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/ImportEnvironment/index.js index 2a4ef2297..e962780c5 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/ImportEnvironment/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/ImportEnvironment/index.js @@ -8,7 +8,7 @@ import { importEnvironment } from 'providers/ReduxStore/slices/collections/actio import { toastError } from 'utils/common/error'; import { IconDatabaseImport } from '@tabler/icons'; -const ImportEnvironment = ({ collection, onClose }) => { +const ImportEnvironment = ({ collection, onClose, onEnvironmentCreated }) => { const dispatch = useDispatch(); const handleImportPostmanEnvironment = () => { @@ -36,17 +36,22 @@ const ImportEnvironment = ({ collection, onClose }) => { }) .then(() => { onClose(); + // Call the callback if provided + if (onEnvironmentCreated) { + onEnvironmentCreated(); + } }) .catch((err) => toastError(err, 'Postman Import environment failed')); }; return ( - + @@ -186,10 +187,10 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
- -
diff --git a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment/index.js b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment/index.js index f28ce36e1..6e56472d8 100644 --- a/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment/index.js +++ b/packages/bruno-app/src/components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment/index.js @@ -9,7 +9,7 @@ import { IconDatabaseImport } from '@tabler/icons'; import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments'; import { uuid } from 'utils/common/index'; -const ImportEnvironment = ({ onClose }) => { +const ImportEnvironment = ({ onClose, onEnvironmentCreated }) => { const dispatch = useDispatch(); const handleImportPostmanEnvironment = () => { @@ -37,17 +37,22 @@ const ImportEnvironment = ({ onClose }) => { }) .then(() => { onClose(); + // Call the callback if provided + if (onEnvironmentCreated) { + onEnvironmentCreated(); + } }) .catch((err) => toastError(err, 'Postman Import environment failed')); }; return ( - +
- +
{generateCodeItemModalOpen && ( diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js index 447523fdb..ced3da6d5 100644 --- a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js +++ b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js @@ -2,7 +2,6 @@ import React from 'react'; import { uuid } from 'utils/common'; import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons'; import EnvironmentSelector from 'components/Environments/EnvironmentSelector'; -import GlobalEnvironmentSelector from 'components/GlobalEnvironments/EnvironmentSelector'; import { addTab } from 'providers/ReduxStore/slices/tabs'; import { useDispatch } from 'react-redux'; import ToolHint from 'components/ToolHint'; @@ -69,9 +68,8 @@ const CollectionToolBar = ({ collection }) => { - + - diff --git a/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js b/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js index 37f673636..be7e7ecbf 100644 --- a/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js +++ b/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js @@ -16,7 +16,7 @@ const StatusCode = ({ status }) => { }; return ( - + {status} {statusCodePhraseMap[status]} ); diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js index 7d1c6599d..1db027ce9 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js @@ -153,7 +153,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => { ] return ( - +

Import from file

diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js index 15410cbcd..243a51f56 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js @@ -48,7 +48,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => const onSubmit = () => formik.handleSubmit(); return ( - +
e.preventDefault()}>