redesign: workspace overview (#6361)

* redesign: workspace overview

* fixes

* fix: test
This commit is contained in:
naman-bruno
2025-12-10 02:56:28 +05:30
committed by GitHub
parent a798b32f25
commit 43f24ad0f1
11 changed files with 1016 additions and 841 deletions

View File

@@ -1,28 +1,48 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.workspace-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
position: relative;
}
.workspace-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: ${(props) => props.theme.text};
}
.workspace-rename-container {
height: 28px;
height: 26px;
display: flex;
align-items: center;
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
gap: 8px;
gap: 6px;
border-radius: 4px;
}
.workspace-name-input {
padding: 0 8px;
font-size: 18px;
font-size: 15px;
font-weight: 600;
border-radius: 4px;
background: transparent;
color: ${(props) => props.theme.text.primary};
color: ${(props) => props.theme.text};
outline: none;
min-width: 200px;
min-width: 180px;
&:focus {
outline: none;
@@ -31,7 +51,7 @@ const StyledWrapper = styled.div`
.inline-actions {
display: flex;
gap: 4px;
gap: 2px;
}
.inline-action-btn {
@@ -41,21 +61,21 @@ const StyledWrapper = styled.div`
padding: 4px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
transition: all 0.15s;
&.save {
color: ${(props) => props.theme.colors.text.green};
&:hover {
background: ${(props) => props.theme.colors.bg.green};
background: ${(props) => props.theme.colors.text.green}1A;
}
}
&.cancel {
color: ${(props) => props.theme.colors.text.red};
color: ${(props) => props.theme.colors.text.danger};
&:hover {
background: ${(props) => props.theme.colors.bg.red};
background: ${(props) => props.theme.colors.text.danger}1A;
}
}
}
@@ -63,71 +83,61 @@ const StyledWrapper = styled.div`
.workspace-error {
position: absolute;
top: 100%;
left: 16px;
left: 0;
margin-top: 4px;
font-size: 12px;
color: ${(props) => props.theme.colors.text.red};
font-size: 11px;
color: ${(props) => props.theme.colors.text.danger};
}
.workspace-menu-dropdown {
min-width: 150px;
min-width: 140px;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
padding: 6px 10px;
cursor: pointer;
transition: background 0.2s;
color: ${(props) => props.theme.text.primary};
transition: background 0.15s;
color: ${(props) => props.theme.text};
font-size: ${(props) => props.theme.font.size.sm};
&.disabled {
opacity: 0.5;
cursor: not-allowed;
&:hover {
background: ${(props) => props.theme.listItem.hoverBg};
}
}
.tabs-container {
display: flex;
gap: 16px;
padding: 0 16px;
border-bottom: 1px solid ${(props) => props.theme.workspace.border};
background: ${(props) => props.theme.bg.primary};
}
.tab-item {
position: relative;
cursor: pointer;
color: var(--color-tab-inactive);
padding: 8px 0;
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
background: none;
border: none;
border-bottom: 2px solid transparent;
transition: all 0.15s ease;
cursor: pointer;
transition: all 0.15s;
&:hover {
color: ${(props) => props.theme.text.primary};
border-bottom-color: ${(props) => props.theme.colors.border};
color: ${(props) => props.theme.text};
}
&.active {
border-bottom-color: ${(props) => props.theme.colors.text.yellow};
color: ${(props) => props.theme.tabs.active.color};
color: ${(props) => props.theme.text};
border-bottom-color: ${(props) => props.theme.workspace.accent};
}
}
.workspace-action-buttons {
gap: 4px;
}
.workspace-button {
display: flex;
align-items: center;
gap: 5px;
padding: 4px 8px;
font-size: 12px;
border-radius: 8px;
color: ${(props) => props.theme.text.primary};
cursor: pointer;
&:hover {
background-color: ${(props) => props.theme.workspace.button.bg};
}
.tab-content {
flex: 1;
overflow: hidden;
}
`;

View File

@@ -1,154 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.collections-table {
display: flex;
flex-direction: column;
height: 100%;
font-size: ${(props) => props.theme.font.size.base};
}
.collections-header {
display: grid;
gap: 16px;
padding: 10px 16px;
border-bottom: ${(props) => props.theme.workspace.collection.header.indentBorder};
position: sticky;
top: 0;
z-index: 10;
&:has(.header-git) {
grid-template-columns: 1fr 3fr 1fr 1.5fr;
}
&:not(:has(.header-git)) {
grid-template-columns: 1fr 3fr 1.5fr;
}
}
.header-cell {
font-weight: 600;
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.text.muted};
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
}
.collections-body {
flex: 1;
overflow-y: auto;
}
.collection-row {
display: grid;
gap: 16px;
padding: 8px 16px;
border-bottom: ${(props) => props.theme.workspace.collection.item.indentBorder};
transition: background-color 0.15s ease;
cursor: pointer;
grid-template-columns: 1fr 3fr 1.5fr;
&:hover {
background-color: ${(props) => props.theme.sidebar.bg};
}
&:last-child {
border-bottom: none;
}
}
.row-cell {
display: flex;
align-items: center;
overflow: hidden;
}
.cell-name {
.collection-icon {
color: ${(props) => props.theme.workspace.accent};
flex-shrink: 0;
}
.collection-info {
min-width: 0;
flex: 1;
}
.collection-name {
font-weight: 400;
color: ${(props) => props.theme.text};
font-size: ${(props) => props.theme.font.size.base};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.collection-subtitle {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.text.muted};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
}
}
.cell-location {
.location-text {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.text.muted};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
}
.cell-actions {
justify-content: flex-end;
.action-buttons {
display: flex;
gap: 4px;
}
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border: none;
background: transparent;
border-radius: 5px;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
color: ${(props) => props.theme.text.muted};
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
&:hover:not(:disabled) {
background-color: ${(props) => props.theme.listItem.hoverBg};
&.action-edit {
color: ${(props) => props.theme.text};
}
&.action-share {
color: #3B82F6;
}
&.action-delete {
color: #EF4444;
}
}
}
`;
export default StyledWrapper;

View File

@@ -1,351 +0,0 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { IconBox, IconTrash, IconEdit, IconShare } from '@tabler/icons';
import { removeCollectionFromWorkspaceAction, importCollectionInWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { hideHomePage } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast';
import Modal from 'components/Modal';
import CreateCollection from 'components/Sidebar/CreateCollection';
import ImportCollection from 'components/Sidebar/ImportCollection';
import RenameCollection from 'components/Sidebar/Collections/Collection/RenameCollection';
import ShareCollection from 'components/ShareCollection';
import StyledWrapper from './StyledWrapper';
import { mountCollection } from 'providers/ReduxStore/slices/collections/actions';
import { normalizePath } from 'utils/common/path';
const WorkspaceCollections = ({ workspace, onImportCollection }) => {
const dispatch = useDispatch();
const { collections } = useSelector((state) => state.collections);
const [collectionToRemove, setCollectionToRemove] = useState(null);
const [renameCollectionModalOpen, setRenameCollectionModalOpen] = useState(false);
const [shareCollectionModalOpen, setShareCollectionModalOpen] = useState(false);
const [selectedCollectionUid, setSelectedCollectionUid] = useState(null);
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const handleImportCollection = ({ rawData, type }) => {
if (onImportCollection) {
onImportCollection();
return;
}
setImportCollectionModalOpen(false);
dispatch(importCollectionInWorkspace(rawData, workspace.uid, undefined, type))
.catch((err) => {
console.error(err);
toast.error('An error occurred while importing the collection');
});
};
const workspaceCollections = React.useMemo(() => {
if (!workspace.collections || workspace.collections.length === 0) {
return [];
}
const result = [];
workspace.collections.forEach((wc) => {
const loadedCollection = collections.find((c) => normalizePath(c.pathname) === normalizePath(wc.path));
if (loadedCollection) {
result.push({
...loadedCollection,
isGitBacked: !!wc.remote,
gitRemoteUrl: wc.remote
});
} else {
result.push({
uid: `unloaded-${wc.path}`,
name: wc.name,
pathname: wc.path,
items: [],
environments: [],
isGitBacked: !!wc.remote,
isLoaded: false,
gitRemoteUrl: wc.remote,
git: { gitRootPath: null },
brunoConfig: {},
root: {
request: {
headers: [],
auth: { mode: 'none' },
vars: { req: [], res: [] },
script: { req: '', res: '' },
tests: ''
},
docs: ''
}
});
}
});
return result;
}, [workspace.collections, collections, workspace.pathname]);
const handleOpenCollectionClick = (collection, event) => {
if (event.target.closest('.action-buttons')) {
return;
}
if (collection.isLoaded === false) {
if (collection.isGitBacked) {
toast.error(`Collection "${collection.name}" needs to be cloned first`);
} else {
toast.error(`Collection "${collection.name}" does not exist on disk`);
}
return;
}
dispatch(mountCollection({
collectionUid: collection.uid,
collectionPathname: collection.pathname,
brunoConfig: collection.brunoConfig
}));
dispatch(hideHomePage());
dispatch(addTab({
uid: collection.uid,
collectionUid: collection.uid,
type: 'collection-settings'
}));
};
const handleRenameCollection = (collection) => {
if (collection.isLoaded === false) {
toast.error('Cannot rename collections that are not cloned yet');
return;
}
setSelectedCollectionUid(collection.uid);
setRenameCollectionModalOpen(true);
};
const handleShareCollection = (collection) => {
if (collection.isLoaded === false) {
toast.error('Please clone this collection first before sharing it');
return;
}
dispatch(mountCollection({
collectionUid: collection.uid,
collectionPathname: collection.pathname,
brunoConfig: collection.brunoConfig
}));
setSelectedCollectionUid(collection.uid);
setShareCollectionModalOpen(true);
};
const handleRemoveCollection = (collection) => {
setCollectionToRemove(collection);
};
const confirmRemoveCollection = async () => {
if (!collectionToRemove) return;
try {
const collectionInfo = getCollectionWorkspaceInfo(collectionToRemove);
const isDelete = collectionInfo.isInternal && !collectionInfo.isGitBacked;
await dispatch(removeCollectionFromWorkspaceAction(workspace.uid, collectionToRemove.pathname));
if (isDelete) {
toast.success(`Deleted "${collectionToRemove.name}" collection`);
} else {
toast.success(`Removed "${collectionToRemove.name}" from workspace`);
}
setCollectionToRemove(null);
} catch (error) {
console.error('Error removing collection:', error);
toast.error(error.message || 'Failed to remove collection from workspace');
}
};
const isInternalCollection = (collection) => {
if (!workspace.pathname || !collection.pathname) return false;
const workspaceCollectionsFolder = normalizePath(`${workspace.pathname}/collections`);
const collectionPath = normalizePath(collection.pathname);
return collectionPath.startsWith(workspaceCollectionsFolder);
};
const getCollectionWorkspaceInfo = (collection) => {
if (collection.hasOwnProperty('isGitBacked')) {
return {
isGitBacked: collection.isGitBacked,
gitRemoteUrl: collection.gitRemoteUrl,
isLoaded: collection.isLoaded !== false,
isInternal: isInternalCollection(collection)
};
}
const workspaceCollection = workspace.collections?.find((wc) => {
return normalizePath(collection.pathname) === normalizePath(wc.path);
});
return {
isGitBacked: !!workspaceCollection?.remote,
gitRemoteUrl: workspaceCollection?.remote,
isLoaded: true,
isInternal: isInternalCollection(collection)
};
};
return (
<StyledWrapper className="h-full">
<div className="w-full h-full">
{createCollectionModalOpen && (
<CreateCollection
onClose={() => setCreateCollectionModalOpen(false)}
/>
)}
{importCollectionModalOpen && (
<ImportCollection
onClose={() => setImportCollectionModalOpen(false)}
handleSubmit={handleImportCollection}
/>
)}
{renameCollectionModalOpen && selectedCollectionUid && (
<RenameCollection
collectionUid={selectedCollectionUid}
onClose={() => {
setRenameCollectionModalOpen(false);
setSelectedCollectionUid(null);
}}
/>
)}
{shareCollectionModalOpen && selectedCollectionUid && (
<ShareCollection
collectionUid={selectedCollectionUid}
onClose={() => {
setShareCollectionModalOpen(false);
setSelectedCollectionUid(null);
}}
/>
)}
{collectionToRemove && (() => {
const collectionInfo = getCollectionWorkspaceInfo(collectionToRemove);
const isDelete = collectionInfo.isInternal && !collectionInfo.isGitBacked;
return (
<Modal
size="sm"
title={isDelete ? 'Delete Collection' : 'Remove Collection'}
handleCancel={() => setCollectionToRemove(null)}
handleConfirm={confirmRemoveCollection}
confirmText={isDelete ? 'Delete' : 'Remove'}
cancelText="Cancel"
style="new"
>
<p className="text-gray-600 dark:text-gray-300">
Are you sure you want to {isDelete ? 'delete' : 'remove'} <strong>"{collectionToRemove.name}"</strong>?
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-3">
{isDelete
? 'This will permanently delete the collection files from the workspace collections folder.'
: 'This will remove the collection from the workspace. The collection files will not be deleted.'}
</p>
</Modal>
);
})()}
<div className="h-full overflow-auto">
{workspaceCollections.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center p-8">
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-full mb-4">
<IconBox size={32} stroke={1.5} className="text-gray-400" />
</div>
<h3 className="text-lg font-medium mb-2">No collections yet</h3>
<p className="text-muted mb-4">
Create your first collection or open an existing one to get started.
</p>
</div>
) : (
<div className="collections-table">
<div className="collections-header">
<div className="header-cell header-name">Collection</div>
<div className="header-cell header-location">Location</div>
<div className="header-cell flex justify-end">Actions</div>
</div>
<div className="collections-body">
{workspaceCollections.map((collection, index) => {
return (
<div
key={collection.uid || index}
className="collection-row"
onClick={(e) => handleOpenCollectionClick(collection, e)}
>
<div className="row-cell cell-name">
<div className="flex items-center gap-2">
<IconBox size={16} stroke={1.5} className="collection-icon" />
<div className="collection-info">
<div className="collection-name">{collection.name}</div>
{collection.brunoConfig?.name && collection.brunoConfig.name !== collection.name && (
<div className="collection-subtitle">{collection.brunoConfig.name}</div>
)}
</div>
</div>
</div>
<div className="row-cell cell-location">
<div className="location-text" title={collection.pathname}>
{collection.pathname}
</div>
</div>
<div className="row-cell cell-actions">
<div className="action-buttons">
<button
onClick={(e) => {
e.stopPropagation();
handleRenameCollection(collection);
}}
className="action-btn action-edit"
title="Rename collection"
>
<IconEdit size={16} stroke={1.5} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleShareCollection(collection);
}}
className="action-btn action-share"
title="Share collection"
>
<IconShare size={16} stroke={1.5} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleRemoveCollection(collection);
}}
className="action-btn action-delete"
title="Remove from workspace"
>
<IconTrash size={16} stroke={1.5} />
</button>
</div>
</div>
</div>
);
})}
</div>
</div>
)}
</div>
</div>
</StyledWrapper>
);
};
export default WorkspaceCollections;

View File

@@ -1,8 +1,145 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.editing-mode {
display: flex;
flex-direction: column;
height: 100%;
.docs-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid ${(props) => props.theme.workspace.border};
}
.docs-title {
display: flex;
align-items: center;
gap: 6px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
color: ${(props) => props.theme.text};
}
.edit-btn {
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
color: ${(props) => props.theme.colors.text.muted};
cursor: pointer;
transition: all 0.15s ease;
}
.docs-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.editor-container {
display: flex;
flex-direction: column;
height: 100%;
}
.editor-actions {
display: flex;
justify-content: flex-end;
padding-top: 10px;
}
.save-btn {
padding: 6px 14px;
background: ${(props) => props.theme.button.secondary.bg};
color: ${(props) => props.theme.button.secondary.color};
border: 1px solid ${(props) => props.theme.button.secondary.border};
border-radius: ${(props) => props.theme.border.radius.sm};
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
border-color: ${(props) => props.theme.button.secondary.hoverBorder};
}
}
.docs-markdown {
height: 100%;
overflow-y: auto;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 32px 16px;
height: 100%;
}
.empty-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 8px;
background: ${(props) => props.theme.workspace.card.bg};
border: 1px solid ${(props) => props.theme.workspace.border};
color: ${(props) => props.theme.colors.text.muted};
margin-bottom: 16px;
}
.empty-text {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
margin-bottom: 2px;
line-height: 1.4;
}
.empty-subtext {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
margin-bottom: 8px;
}
.suggestions-list {
list-style: none;
padding: 0;
margin: 0 0 20px 0;
text-align: center;
li {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
padding: 2px 0;
&::before {
content: '\\2022';
color: ${(props) => props.theme.workspace.accent};
margin-right: 6px;
}
}
}
.add-docs-btn {
padding: 8px 16px;
background: transparent;
color: ${(props) => props.theme.workspace.accent};
border: 1px solid ${(props) => props.theme.workspace.accent};
border-radius: ${(props) => props.theme.border.radius.base};
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background: ${(props) => props.theme.workspace.accent}14;
}
}
`;

View File

@@ -7,7 +7,7 @@ import { saveWorkspaceDocs } from 'providers/ReduxStore/slices/workspaces/action
import Markdown from 'components/MarkDown';
import CodeEditor from 'components/CodeEditor';
import StyledWrapper from './StyledWrapper';
import { IconEdit, IconX, IconFileText } from '@tabler/icons';
import { IconFileText, IconEdit, IconX } from '@tabler/icons';
import toast from 'react-hot-toast';
const WorkspaceDocs = ({ workspace }) => {
@@ -51,31 +51,34 @@ const WorkspaceDocs = ({ workspace }) => {
}
};
const handleAddDocumentation = () => {
setIsEditing(true);
};
const hasDocs = localDocs && localDocs.trim().length > 0;
return (
<StyledWrapper className="h-full w-full relative flex flex-col p-4">
<div className="flex flex-row w-full justify-between items-center mb-4">
<div className="text-lg font-medium flex items-center gap-2">
<IconFileText size={20} strokeWidth={1.5} />
Workspace Documentation
<StyledWrapper className="h-full w-full flex flex-col">
<div className="docs-header">
<div className="docs-title">
<IconFileText size={16} strokeWidth={1.5} />
<span>Documentation</span>
</div>
<div className="flex flex-row gap-2 items-center justify-center">
{isEditing ? (
<>
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
<IconX className="cursor-pointer" size={20} strokeWidth={1.5} />
</div>
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={onSave}>
Save
{hasDocs && !isEditing && (
<button className="edit-btn" onClick={toggleViewMode}>
<IconEdit size={14} strokeWidth={1.5} />
</button>
)}
{isEditing && (
<button className="edit-btn" onClick={handleDiscardChanges}>
<IconX size={14} strokeWidth={1.5} />
</button>
</>
) : (
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />
</div>
)}
</div>
</div>
<div className="docs-content">
{isEditing ? (
<div className="editor-container">
<CodeEditor
theme={displayedTheme}
value={localDocs}
@@ -85,63 +88,39 @@ const WorkspaceDocs = ({ workspace }) => {
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
/>
) : (
<div className="h-full overflow-auto pl-1">
<div className="h-[1px] min-h-[500px]">
{
localDocs?.length > 0
? <Markdown onDoubleClick={toggleViewMode} content={localDocs} />
: <Markdown onDoubleClick={toggleViewMode} content={workspaceDocumentationPlaceholder} />
}
<div className="editor-actions">
<button className="save-btn" onClick={onSave}>
Save
</button>
</div>
</div>
) : hasDocs ? (
<div className="docs-markdown">
<Markdown collectionPath={workspace?.pathname || ''} onDoubleClick={toggleViewMode} content={localDocs} />
</div>
) : (
<div className="empty-state">
<div className="empty-icon-wrapper">
<IconFileText size={28} strokeWidth={1} />
</div>
<p className="empty-text">
Add documentation to help your team work smoothly.
</p>
<p className="empty-subtext">You can include:</p>
<ul className="suggestions-list">
<li>Project overview</li>
<li>Setup instructions</li>
<li>Key workflows</li>
<li>Resources & FAQs</li>
</ul>
<button className="add-docs-btn" onClick={handleAddDocumentation}>
Add Documentation
</button>
</div>
)}
</div>
</StyledWrapper>
);
};
export default WorkspaceDocs;
const workspaceDocumentationPlaceholder = `
# Welcome to your Workspace Documentation
This is your workspace documentation area where you can document your entire project, team guidelines, and shared resources.
## What to Document Here
### Project Overview
- Project goals and objectives
- Architecture overview
- Key stakeholders and team members
- Project timeline and milestones
### Development Guidelines
- Coding standards and conventions
- Git workflow and branching strategy
- Code review process
- Testing guidelines
### API Documentation
- Authentication methods
- Base URLs and environments
- Common headers and parameters
- Error handling standards
### Team Resources
- Useful links and references
- Development environment setup
- Deployment procedures
- Troubleshooting guides
## Markdown Support
This documentation supports full Markdown formatting:
- **Bold** and *italic* text
- \`inline code\` and code blocks
- Lists and tables
- [Links](https://usebruno.com) and images
- Headers and sections
**Tip:** Double-click anywhere in this area to start editing!
`;

View File

@@ -0,0 +1,123 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
flex: 1;
overflow-y: auto;
.collections-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 20px;
text-align: center;
}
.empty-icon {
color: ${(props) => props.theme.colors.text.muted};
margin-bottom: 12px;
}
.empty-title {
font-size: ${(props) => props.theme.font.size.md};
font-weight: 500;
color: ${(props) => props.theme.text};
margin-bottom: 6px;
}
.empty-description {
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.colors.text.muted};
}
.collection-card {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
background: ${(props) => props.theme.workspace.card.bg};
border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};
cursor: pointer;
&:last-child {
border-bottom: none;
}
}
.collection-icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
color: ${(props) => props.theme.workspace.accent};
flex-shrink: 0;
}
.collection-info {
flex: 1;
min-width: 0;
}
.collection-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 1px;
}
.collection-name {
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.text};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.collection-path {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.collection-menu {
flex-shrink: 0;
color: ${(props) => props.theme.colors.text.muted};
cursor: pointer;
&:hover {
color: ${(props) => props.theme.text};
}
}
.collection-dropdown {
min-width: 120px;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
cursor: pointer;
transition: background 0.15s ease;
color: ${(props) => props.theme.text};
font-size: ${(props) => props.theme.font.size.sm};
&:hover {
background: ${(props) => props.theme.listItem.hoverBg};
}
&.dropdown-item-danger {
color: ${(props) => props.theme.colors.text.danger};
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,314 @@
import React, { useState, useMemo, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { IconBox, IconTrash, IconEdit, IconShare, IconDots } from '@tabler/icons';
import { removeCollectionFromWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { hideHomePage } from 'providers/ReduxStore/slices/app';
import { mountCollection } from 'providers/ReduxStore/slices/collections/actions';
import { normalizePath } from 'utils/common/path';
import toast from 'react-hot-toast';
import Modal from 'components/Modal';
import RenameCollection from 'components/Sidebar/Collections/Collection/RenameCollection';
import ShareCollection from 'components/ShareCollection';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
const CollectionsList = ({ workspace }) => {
const dispatch = useDispatch();
const { collections } = useSelector((state) => state.collections);
const dropdownRefs = useRef({});
const [collectionToRemove, setCollectionToRemove] = useState(null);
const [renameCollectionModalOpen, setRenameCollectionModalOpen] = useState(false);
const [shareCollectionModalOpen, setShareCollectionModalOpen] = useState(false);
const [selectedCollectionUid, setSelectedCollectionUid] = useState(null);
const workspaceCollections = useMemo(() => {
if (!workspace.collections || workspace.collections.length === 0) {
return [];
}
return workspace.collections.map((wc) => {
const loadedCollection = collections.find(
(c) => normalizePath(c.pathname) === normalizePath(wc.path)
);
if (loadedCollection) {
return {
...loadedCollection,
isGitBacked: !!wc.remote,
gitRemoteUrl: wc.remote
};
}
return {
uid: `unloaded-${wc.path}`,
name: wc.name,
pathname: wc.path,
items: [],
environments: [],
isGitBacked: !!wc.remote,
isLoaded: false,
gitRemoteUrl: wc.remote,
git: { gitRootPath: null },
brunoConfig: {},
root: {
request: {
headers: [],
auth: { mode: 'none' },
vars: { req: [], res: [] },
script: { req: '', res: '' },
tests: ''
},
docs: ''
}
};
});
}, [workspace.collections, collections]);
const isInternalCollection = (collection) => {
if (!workspace.pathname || !collection.pathname) return false;
const workspaceCollectionsFolder = normalizePath(`${workspace.pathname}/collections`);
const collectionPath = normalizePath(collection.pathname);
return collectionPath.startsWith(workspaceCollectionsFolder);
};
const getCollectionWorkspaceInfo = (collection) => {
if (Object.prototype.hasOwnProperty.call(collection, 'isGitBacked')) {
return {
isGitBacked: collection.isGitBacked,
gitRemoteUrl: collection.gitRemoteUrl,
isLoaded: collection.isLoaded !== false,
isInternal: isInternalCollection(collection)
};
}
const workspaceCollection = workspace.collections?.find(
(wc) => normalizePath(collection.pathname) === normalizePath(wc.path)
);
return {
isGitBacked: !!workspaceCollection?.remote,
gitRemoteUrl: workspaceCollection?.remote,
isLoaded: true,
isInternal: isInternalCollection(collection)
};
};
const handleOpenCollectionClick = (collection, event) => {
if (event.target.closest('.collection-menu')) {
return;
}
if (collection.isLoaded === false) {
if (collection.isGitBacked) {
toast.error(`Collection "${collection.name}" needs to be cloned first`);
} else {
toast.error(`Collection "${collection.name}" does not exist on disk`);
}
return;
}
dispatch(
mountCollection({
collectionUid: collection.uid,
collectionPathname: collection.pathname,
brunoConfig: collection.brunoConfig
})
);
dispatch(hideHomePage());
dispatch(
addTab({
uid: collection.uid,
collectionUid: collection.uid,
type: 'collection-settings'
})
);
};
const handleRenameCollection = (collection) => {
dropdownRefs.current[collection.uid]?.hide();
if (collection.isLoaded === false) {
toast.error('Cannot rename collections that are not cloned yet');
return;
}
setSelectedCollectionUid(collection.uid);
setRenameCollectionModalOpen(true);
};
const handleShareCollection = (collection) => {
dropdownRefs.current[collection.uid]?.hide();
if (collection.isLoaded === false) {
toast.error('Please clone this collection first before sharing it');
return;
}
dispatch(
mountCollection({
collectionUid: collection.uid,
collectionPathname: collection.pathname,
brunoConfig: collection.brunoConfig
})
);
setSelectedCollectionUid(collection.uid);
setShareCollectionModalOpen(true);
};
const handleRemoveCollection = (collection) => {
dropdownRefs.current[collection.uid]?.hide();
setCollectionToRemove(collection);
};
const confirmRemoveCollection = async () => {
if (!collectionToRemove) return;
try {
const collectionInfo = getCollectionWorkspaceInfo(collectionToRemove);
const isDelete = collectionInfo.isInternal && !collectionInfo.isGitBacked;
await dispatch(removeCollectionFromWorkspaceAction(workspace.uid, collectionToRemove.pathname));
if (isDelete) {
toast.success(`Deleted "${collectionToRemove.name}" collection`);
} else {
toast.success(`Removed "${collectionToRemove.name}" from workspace`);
}
setCollectionToRemove(null);
} catch (error) {
console.error('Error removing collection:', error);
toast.error(error.message || 'Failed to remove collection from workspace');
}
};
const renderRemoveModal = () => {
if (!collectionToRemove) return null;
const collectionInfo = getCollectionWorkspaceInfo(collectionToRemove);
const isDelete = collectionInfo.isInternal && !collectionInfo.isGitBacked;
return (
<Modal
size="sm"
title={isDelete ? 'Delete Collection' : 'Remove Collection'}
handleCancel={() => setCollectionToRemove(null)}
handleConfirm={confirmRemoveCollection}
confirmText={isDelete ? 'Delete' : 'Remove'}
cancelText="Cancel"
style="new"
>
<p className="text-gray-600 dark:text-gray-300">
Are you sure you want to {isDelete ? 'delete' : 'remove'}{' '}
<strong>"{collectionToRemove.name}"</strong>?
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-3">
{isDelete
? 'This will permanently delete the collection files from the workspace collections folder.'
: 'This will remove the collection from the workspace. The collection files will not be deleted.'}
</p>
</Modal>
);
};
return (
<StyledWrapper>
{renameCollectionModalOpen && selectedCollectionUid && (
<RenameCollection
collectionUid={selectedCollectionUid}
onClose={() => {
setRenameCollectionModalOpen(false);
setSelectedCollectionUid(null);
}}
/>
)}
{shareCollectionModalOpen && selectedCollectionUid && (
<ShareCollection
collectionUid={selectedCollectionUid}
onClose={() => {
setShareCollectionModalOpen(false);
setSelectedCollectionUid(null);
}}
/>
)}
{renderRemoveModal()}
<div className="collections-list">
{workspaceCollections.length === 0 ? (
<div className="empty-state">
<IconBox size={32} strokeWidth={1.5} className="empty-icon" />
<h3 className="empty-title">No collections yet</h3>
<p className="empty-description">
Create your first collection or open an existing one to get started.
</p>
</div>
) : (
workspaceCollections.map((collection, index) => (
<div
key={collection.uid || index}
className="collection-card"
onClick={(e) => handleOpenCollectionClick(collection, e)}
>
<div className="collection-info">
<div className="collection-header">
<div className="collection-icon-wrapper">
<IconBox size={18} strokeWidth={1.5} />
</div>
<div className="collection-name">{collection.name}</div>
</div>
<div className="collection-path">{collection.pathname}</div>
</div>
<div className="collection-menu">
<Dropdown
style="new"
placement="bottom-end"
onCreate={(ref) => (dropdownRefs.current[collection.uid] = ref)}
icon={<IconDots size={18} strokeWidth={1.5} />}
>
<div className="collection-dropdown">
<div
className="dropdown-item"
onClick={(e) => {
e.stopPropagation();
handleRenameCollection(collection);
}}
>
<IconEdit size={16} strokeWidth={1.5} />
<span>Rename</span>
</div>
<div
className="dropdown-item"
onClick={(e) => {
e.stopPropagation();
handleShareCollection(collection);
}}
>
<IconShare size={16} strokeWidth={1.5} />
<span>Share</span>
</div>
<div
className="dropdown-item dropdown-item-danger"
onClick={(e) => {
e.stopPropagation();
handleRemoveCollection(collection);
}}
>
<IconTrash size={16} strokeWidth={1.5} />
<span>Remove</span>
</div>
</div>
</Dropdown>
</div>
</div>
))
)}
</div>
</StyledWrapper>
);
};
export default CollectionsList;

View File

@@ -0,0 +1,92 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
height: 100%;
.overview-layout {
display: flex;
height: 100%;
}
.overview-main {
flex: 3;
padding: 20px 16px 16px;
overflow-y: auto;
border-right: 1px solid ${(props) => props.theme.workspace.border};
}
.overview-docs {
display: flex;
flex: 2;
flex-direction: column;
overflow: hidden;
}
.stats-row {
display: flex;
gap: 24px;
margin-bottom: 16px;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 2px;
}
.stat-value {
font-size: 22px;
font-weight: 600;
color: ${(props) => props.theme.text};
line-height: 1;
}
.stat-label {
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.colors.text.muted};
}
.quick-actions-section {
margin-bottom: 16px;
}
.section-title {
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
color: ${(props) => props.theme.colors.text.muted};
margin-bottom: 8px;
}
.quick-actions-buttons {
display: flex;
gap: 8px;
}
.quick-action-btn {
display: flex;
align-items: center;
gap: 5px;
padding: 4px 8px;
border: 1px solid ${(props) => props.theme.workspace.accent};
border-radius: ${(props) => props.theme.border.radius.base};
background: transparent;
color: ${(props) => props.theme.workspace.accent};
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background: ${(props) => props.theme.workspace.accent}14;
}
}
.collections-section {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,127 @@
import React, { useState, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { IconPlus, IconFolder, IconFileImport } from '@tabler/icons';
import { importCollectionInWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
import { openCollection } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import CreateCollection from 'components/Sidebar/CreateCollection';
import ImportCollection from 'components/Sidebar/ImportCollection';
import CollectionsList from './CollectionsList';
import WorkspaceDocs from '../WorkspaceDocs';
import StyledWrapper from './StyledWrapper';
const WorkspaceOverview = ({ workspace }) => {
const dispatch = useDispatch();
const { collections } = useSelector((state) => state.collections);
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const workspaceCollectionsCount = workspace?.collections?.length || 0;
const workspaceEnvironmentsCount = useMemo(() => {
if (!workspace?.collections || !collections) return 0;
let count = 0;
workspace.collections.forEach((wc) => {
const loadedCollection = collections.find((c) => c.pathname === wc.path);
if (loadedCollection?.environments) {
count += loadedCollection.environments.length;
}
});
return count;
}, [workspace?.collections, collections]);
const handleCreateCollection = async () => {
if (!workspace?.pathname) {
toast.error('Workspace path not found');
return;
}
try {
const { ipcRenderer } = window;
await ipcRenderer.invoke('renderer:ensure-collections-folder', workspace.pathname);
setCreateCollectionModalOpen(true);
} catch (error) {
console.error('Error ensuring collections folder exists:', error);
toast.error('Error preparing workspace for collection creation');
}
};
const handleOpenCollection = () => {
dispatch(openCollection()).catch((err) => {
console.error(err);
toast.error('An error occurred while opening the collection');
});
};
const handleImportCollection = () => {
setImportCollectionModalOpen(true);
};
const handleImportCollectionSubmit = ({ rawData, type }) => {
setImportCollectionModalOpen(false);
dispatch(importCollectionInWorkspace(rawData, workspace.uid, undefined, type)).catch((err) => {
console.error(err);
toast.error('An error occurred while importing the collection');
});
};
return (
<StyledWrapper>
{createCollectionModalOpen && (
<CreateCollection onClose={() => setCreateCollectionModalOpen(false)} />
)}
{importCollectionModalOpen && (
<ImportCollection
onClose={() => setImportCollectionModalOpen(false)}
handleSubmit={handleImportCollectionSubmit}
/>
)}
<div className="overview-layout">
<div className="overview-main">
<div className="stats-row">
<div className="stat-item">
<span className="stat-value">{workspaceCollectionsCount}</span>
<span className="stat-label">Collections</span>
</div>
<div className="stat-item">
<span className="stat-value">{workspaceEnvironmentsCount}</span>
<span className="stat-label">Environments</span>
</div>
</div>
<div className="quick-actions-section">
<div className="section-title">Quick Actions</div>
<div className="quick-actions-buttons">
<button className="quick-action-btn" onClick={handleCreateCollection}>
<IconPlus size={14} strokeWidth={1.5} />
<span>Create Collection</span>
</button>
<button className="quick-action-btn" onClick={handleOpenCollection}>
<IconFolder size={14} strokeWidth={1.5} />
<span>Open Collection</span>
</button>
<button className="quick-action-btn" onClick={handleImportCollection}>
<IconFileImport size={14} strokeWidth={1.5} />
<span>Import Collection</span>
</button>
</div>
</div>
<div className="collections-section">
<div className="section-title">Collections</div>
<CollectionsList workspace={workspace} />
</div>
</div>
<div className="overview-docs">
<WorkspaceDocs workspace={workspace} />
</div>
</div>
</StyledWrapper>
);
};
export default WorkspaceOverview;

View File

@@ -1,14 +1,11 @@
import React, { useEffect, useState, useRef } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { IconCategory, IconPlus, IconFolders, IconFileImport, IconDots, IconEdit, IconX, IconCheck, IconFolder } from '@tabler/icons';
import { importCollectionInWorkspace, renameWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';
import { showInFolder, openCollection } from 'providers/ReduxStore/slices/collections/actions';
import { IconCategory, IconDots, IconEdit, IconX, IconCheck, IconFolder } from '@tabler/icons';
import { renameWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';
import { showInFolder } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import CreateCollection from 'components/Sidebar/CreateCollection';
import ImportCollection from 'components/Sidebar/ImportCollection';
import CloseWorkspace from 'components/Sidebar/SidebarHeader/CloseWorkspace';
import WorkspaceCollections from './WorkspaceCollections';
import WorkspaceDocs from './WorkspaceDocs';
import WorkspaceOverview from './WorkspaceOverview';
import WorkspaceEnvironments from './WorkspaceEnvironments';
import StyledWrapper from './StyledWrapper';
import Dropdown from 'components/Dropdown';
@@ -16,10 +13,7 @@ import Dropdown from 'components/Dropdown';
const WorkspaceHome = () => {
const dispatch = useDispatch();
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
const [activeTab, setActiveTab] = useState('collections');
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const [activeTab, setActiveTab] = useState('overview');
const [isRenamingWorkspace, setIsRenamingWorkspace] = useState(false);
const [workspaceNameInput, setWorkspaceNameInput] = useState('');
@@ -51,40 +45,8 @@ const WorkspaceHome = () => {
return null;
}
const handleCreateCollection = async () => {
try {
const { ipcRenderer } = window;
await ipcRenderer.invoke('renderer:ensure-collections-folder', activeWorkspace.pathname);
setCreateCollectionModalOpen(true);
} catch (error) {
console.error('Error ensuring collections folder exists:', error);
toast.error('Error preparing workspace for collection creation');
}
};
const handleOpenCollection = () => {
dispatch(openCollection())
.catch((err) => {
console.error(err);
toast.error('An error occurred while opening the collection');
});
};
const handleImportCollection = () => {
setImportCollectionModalOpen(true);
};
const handleImportCollectionSubmit = ({ rawData, type, environment, repositoryUrl }) => {
setImportCollectionModalOpen(false);
dispatch(importCollectionInWorkspace(rawData, activeWorkspace.uid, undefined, type))
.catch((err) => {
console.error(err);
toast.error('An error occurred while importing the collection');
});
};
// Workspace menu handlers
const handleRenameWorkspaceClick = () => {
dropdownTippyRef.current?.hide();
setIsRenamingWorkspace(true);
setWorkspaceNameInput(activeWorkspace.name);
setWorkspaceNameError('');
@@ -106,9 +68,7 @@ const WorkspaceHome = () => {
const handleShowInFolder = () => {
dropdownTippyRef.current?.hide();
if (activeWorkspace.pathname) {
dispatch(showInFolder(activeWorkspace.pathname))
.catch((error) => {
console.error('Error opening the folder', error);
dispatch(showInFolder(activeWorkspace.pathname)).catch((error) => {
toast.error('Error opening the folder');
});
}
@@ -118,15 +78,12 @@ const WorkspaceHome = () => {
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';
}
return null;
};
@@ -157,9 +114,7 @@ const WorkspaceHome = () => {
};
const handleWorkspaceNameChange = (e) => {
const value = e.target.value;
setWorkspaceNameInput(value);
setWorkspaceNameInput(e.target.value);
if (workspaceNameError) {
setWorkspaceNameError('');
}
@@ -175,52 +130,36 @@ const WorkspaceHome = () => {
}
};
if (!activeWorkspace) {
const tabs = [
{ id: 'overview', label: 'Overview' },
{ id: 'environments', label: 'Environments' }
];
const renderTabContent = () => {
switch (activeTab) {
case 'overview':
return <WorkspaceOverview workspace={activeWorkspace} />;
case 'environments':
return <WorkspaceEnvironments workspace={activeWorkspace} />;
default:
return null;
}
const tabs = [
{
id: 'collections',
label: 'Collections',
component: (
<WorkspaceCollections
workspace={activeWorkspace}
onImportCollection={handleImportCollection}
/>
)
},
{
id: 'environments',
label: 'Environments',
component: <WorkspaceEnvironments workspace={activeWorkspace} />
},
{
id: 'documentation',
label: 'Documentation',
component: <WorkspaceDocs workspace={activeWorkspace} />
}
];
};
return (
<StyledWrapper className="h-full">
<div className="h-full flex flex-col">
{createCollectionModalOpen && (
<CreateCollection
onClose={() => setCreateCollectionModalOpen(false)}
<div className="h-full flex flex-row">
{closeWorkspaceModalOpen && (
<CloseWorkspace
workspaceUid={activeWorkspace.uid}
onClose={() => setCloseWorkspaceModalOpen(false)}
/>
)}
{importCollectionModalOpen && (
<ImportCollection
onClose={() => setImportCollectionModalOpen(false)}
handleSubmit={handleImportCollectionSubmit}
/>
)}
<div className="flex items-center gap-5 p-4 pb-2 workspace-header">
<div className="text-xl font-semibold flex items-center gap-2">
<IconCategory size={24} stroke={2} />
<div className="main-content">
<div className="workspace-header">
<div className="workspace-title">
<IconCategory size={20} strokeWidth={1.5} />
{isRenamingWorkspace ? (
<div className="workspace-rename-container" ref={workspaceRenameContainerRef}>
<input
@@ -264,7 +203,7 @@ const WorkspaceHome = () => {
style="new"
placement="bottom-end"
onCreate={onDropdownCreate}
icon={<IconDots size={20} strokeWidth={1.5} className="cursor-pointer" />}
icon={<IconDots size={18} strokeWidth={1.5} className="cursor-pointer" />}
>
<div className="workspace-menu-dropdown">
<div className="dropdown-item" onClick={handleRenameWorkspaceClick}>
@@ -288,60 +227,19 @@ const WorkspaceHome = () => {
)}
</div>
{closeWorkspaceModalOpen && (
<CloseWorkspace
workspaceUid={activeWorkspace.uid}
onClose={() => setCloseWorkspaceModalOpen(false)}
/>
)}
<div className="flex items-center justify-between px-4 tabs-container">
<div className="flex gap-5">
{tabs.map((tab) => {
return (
<div className="tabs-container">
{tabs.map((tab) => (
<button
key={tab.id}
className={`tab-item ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 py-2 text-sm border-b-2 transition-colors tab-item ${activeTab === tab.id ? 'active' : ''}`}
>
{tab.label}
</button>
);
})}
))}
</div>
{activeTab === 'collections' && (
<div className="flex items-center gap-1 workspace-action-buttons">
<button
onClick={handleCreateCollection}
className="workspace-button"
title="Create Collection"
>
<IconPlus size={16} stroke={1.5} />
Create
</button>
<button
onClick={handleOpenCollection}
className="workspace-button"
title="Add Collection"
>
<IconFolders size={16} stroke={1.5} />
Add
</button>
<button
onClick={handleImportCollection}
className="workspace-button"
title="Import Collection"
>
<IconFileImport size={16} stroke={1.5} />
Import
</button>
</div>
)}
</div>
<div className="flex-1 overflow-hidden">
{tabs.find((tab) => tab.id === activeTab)?.component}
<div className="tab-content">{renderTabContent()}</div>
</div>
</div>
</StyledWrapper>

View File

@@ -121,7 +121,7 @@ test.describe('DevTools Performance Tab', () => {
await expect(performanceTab).not.toHaveClass(/active/);
// Verify Console tab content is shown
await expect(page.locator('.tab-content')).toBeVisible();
await expect(page.locator('.console-empty')).toBeVisible();
// Switch back to Performance tab
await performanceTab.click();