mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 22:45:25 +00:00
feat(sandbox): create a dropdown selector for sandbox mode (#6519)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.sandbox-icon {
|
||||
@@ -12,19 +13,156 @@ const StyledWrapper = styled.div`
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
background-color: ${(props) => props.theme.background.surface0};
|
||||
}
|
||||
}
|
||||
|
||||
.safe-mode {
|
||||
background-color: ${(props) => props.theme.app.collection.toolbar.sandboxMode.safeMode.bg};
|
||||
color: ${(props) => props.theme.app.collection.toolbar.sandboxMode.safeMode.color};
|
||||
}
|
||||
|
||||
.developer-mode {
|
||||
background-color: ${(props) => props.theme.app.collection.toolbar.sandboxMode.developerMode.bg};
|
||||
color: ${(props) => props.theme.app.collection.toolbar.sandboxMode.developerMode.color};
|
||||
}
|
||||
|
||||
.sandbox-dropdown {
|
||||
min-width: 260px;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.sandbox-header {
|
||||
padding: 0.5rem 0.625rem;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
color: ${(props) => props.theme.dropdown.headingText};
|
||||
}
|
||||
|
||||
.sandbox-option {
|
||||
display: flex;
|
||||
margin: 5px;
|
||||
border-radius: ${(props) => props.theme.border.radius.md};
|
||||
padding: 12px;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
|
||||
&.safe-mode {
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&.developer-mode {
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
color: ${(props) => props.theme.colors.text.warning};
|
||||
}
|
||||
|
||||
&.active {
|
||||
cursor: default;
|
||||
|
||||
&.developer-mode {
|
||||
border: 1px solid ${(props) => props.theme.colors.text.warning};
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.warning, 0.04)};
|
||||
|
||||
.sandbox-option-radio input:checked {
|
||||
border-color: ${(props) => props.theme.colors.text.warning};
|
||||
}
|
||||
|
||||
.sandbox-option-radio input::after {
|
||||
background: ${(props) => props.theme.colors.text.warning};
|
||||
}
|
||||
}
|
||||
|
||||
&.safe-mode {
|
||||
border: 1px solid ${(props) => props.theme.colors.text.green};
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.green, 0.04)};
|
||||
|
||||
.sandbox-option-radio input:checked {
|
||||
border-color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
.sandbox-option-radio input::after {
|
||||
background: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.recommended-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.green, 0.1)};
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
}
|
||||
|
||||
.sandbox-option-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
gap: 0.25rem;
|
||||
line-height: 1.25rem;
|
||||
color: ${(props) => props.theme.colors.text.subtext2};
|
||||
}
|
||||
|
||||
.sandbox-option-radio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.sandbox-option-radio input {
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid currentColor;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.sandbox-option-radio input::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 3px;
|
||||
border-radius: 9999px;
|
||||
background: ${(props) => props.theme.background.base};
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.sandbox-option-radio input:checked::after {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.sandbox-option-radio input:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sandbox-option-description {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
line-height: 1.1rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.developer-mode-warning {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
display: inline-block;
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.warning, 0.1)};
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
color: ${(props) => props.theme.colors.text.warning};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,45 +1,126 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconShieldCheck, IconCode } from '@tabler/icons';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { uuid } from 'utils/common/index';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
|
||||
const SANDBOX_OPTIONS = [
|
||||
{
|
||||
key: 'safe',
|
||||
label: 'Safe Mode',
|
||||
description: 'JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.',
|
||||
icon: IconShieldCheck,
|
||||
recommended: true
|
||||
},
|
||||
{
|
||||
key: 'developer',
|
||||
label: 'Developer Mode',
|
||||
description: 'JavaScript code has access to the filesystem, can execute system commands and access sensitive information.',
|
||||
icon: IconCode,
|
||||
warning: 'Use only if you trust the authors of the collection',
|
||||
recommended: false
|
||||
}
|
||||
];
|
||||
|
||||
const JsSandboxMode = ({ collection }) => {
|
||||
const jsSandboxMode = collection?.securityConfig?.jsSandboxMode || 'safe';
|
||||
const dispatch = useDispatch();
|
||||
const dropdownRef = useRef(null);
|
||||
const [selectedMode, setSelectedMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedMode(collection?.securityConfig?.jsSandboxMode || 'safe');
|
||||
}, [collection?.securityConfig?.jsSandboxMode]);
|
||||
|
||||
const onDropdownCreate = (instance) => {
|
||||
dropdownRef.current = instance;
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
dropdownRef.current?.hide();
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e && e.key === 'Escape') {
|
||||
closeDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
const handleModeChange = (mode) => {
|
||||
if (!collection?.uid || mode === selectedMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewSecuritySettings = () => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: uuid(),
|
||||
collectionUid: collection.uid,
|
||||
type: 'security-settings'
|
||||
saveCollectionSecurityConfig(collection.uid, {
|
||||
jsSandboxMode: mode
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
setSelectedMode(mode);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
toast.error('Failed to update sandbox mode');
|
||||
});
|
||||
};
|
||||
|
||||
const renderOption = (option) => {
|
||||
const OptionIcon = option.icon;
|
||||
const isActive = selectedMode === option.key;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={option.key}
|
||||
className={`sandbox-option ${option.key}-mode ${isActive ? 'active' : ''}`}
|
||||
onClick={() => handleModeChange(option.key)}
|
||||
role="menuitemradio"
|
||||
aria-checked={isActive}
|
||||
data-testid={`sandbox-mode-${option.key}`}
|
||||
>
|
||||
|
||||
<div className="dropdown-label">
|
||||
<div className="sandbox-option-title">
|
||||
<div className="sandbox-option-radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="sandbox-mode"
|
||||
value={option.key}
|
||||
checked={isActive}
|
||||
/>
|
||||
</div>
|
||||
<OptionIcon size={24} strokeWidth={1.5} />
|
||||
{option.label}
|
||||
{option.recommended && <span className="recommended-badge">Recommended</span>}
|
||||
</div>
|
||||
{option.warning && (<div><span className="developer-mode-warning">{option.warning}</span></div>)}
|
||||
<div className="sandbox-option-description">{option.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const triggerIcon = (
|
||||
<div>
|
||||
<ToolHint text={`${selectedMode === 'developer' ? 'Developer Mode' : 'Safe Mode'}`} toolhintId="JavascriptSandboxToolhintId" place="bottom">
|
||||
<div className={`sandbox-icon ${selectedMode === 'developer' ? 'developer-mode' : 'safe-mode'}`} data-testid="sandbox-mode-selector">
|
||||
{selectedMode === 'developer' ? <IconCode size={14} strokeWidth={2} /> : <IconShieldCheck size={14} strokeWidth={2} />}
|
||||
</div>
|
||||
</ToolHint>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex">
|
||||
{jsSandboxMode === 'safe' && (
|
||||
<div
|
||||
className="sandbox-icon safe-mode"
|
||||
data-testid="sandbox-mode-selector"
|
||||
onClick={viewSecuritySettings}
|
||||
title="Safe Mode"
|
||||
>
|
||||
<IconShieldCheck size={14} strokeWidth={2} />
|
||||
<StyledWrapper className="flex" onKeyDown={handleKeyDown}>
|
||||
<Dropdown onCreate={onDropdownCreate} icon={triggerIcon} placement="bottom-start">
|
||||
<div className="sandbox-dropdown">
|
||||
<div className="sandbox-header">JavaScript Sandbox</div>
|
||||
{SANDBOX_OPTIONS.map(renderOption)}
|
||||
</div>
|
||||
)}
|
||||
{jsSandboxMode === 'developer' && (
|
||||
<div
|
||||
className="sandbox-icon developer-mode"
|
||||
data-testid="sandbox-mode-selector"
|
||||
onClick={viewSecuritySettings}
|
||||
title="Developer Mode"
|
||||
>
|
||||
<IconCode size={14} strokeWidth={2} />
|
||||
</div>
|
||||
)}
|
||||
</Dropdown>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user