feat(sandbox): create a dropdown selector for sandbox mode (#6519)

This commit is contained in:
Bijin A B
2025-12-30 23:03:06 +05:30
committed by GitHub
parent 0848393319
commit 8fa8ae5fed
12 changed files with 278 additions and 190 deletions

View File

@@ -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;

View File

@@ -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>
);
};