mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-07 14:08:38 +00:00
feat: theme + ux overhaul (#6520)
* feat: theme + ux overhaul * chore: moved @opencollection/types at root of monorepo * chore: fixed review comments * chore: addressed review comments * chore: fix playwright tests --------- Co-authored-by: Bijin A B <bijin@usebruno.com>
This commit is contained in:
@@ -52,6 +52,7 @@
|
||||
"json5": "^2.2.3",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"jsonschema": "^1.5.0",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -246,12 +246,6 @@ const Wrapper = styled.div`
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item-active {
|
||||
font-weight: 400 !important;
|
||||
background-color: ${(props) => props.theme.dropdown.selectedBg} !important;
|
||||
color: ${(props) => props.theme.dropdown.selectedColor} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -19,7 +19,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
color: ${(props) => props.theme.brand};
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
|
||||
@@ -5,9 +5,9 @@ const StyledWrapper = styled.div`
|
||||
background-color: ${(props) => props.theme.requestTabPanel.card.bg};
|
||||
|
||||
.title {
|
||||
border-top: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-left: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-right: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-top: 1px solid ${(props) => props.theme.border.BORDER0};
|
||||
border-left: 1px solid ${(props) => props.theme.border.BORDER0};
|
||||
border-right: 1px solid ${(props) => props.theme.border.BORDER0};
|
||||
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
@@ -15,8 +15,8 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.table {
|
||||
thead {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.bg};
|
||||
color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.color};
|
||||
color: ${(props) => props.theme.table.thead.color} !important;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const StyledWrapper = styled.div`
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
|
||||
@@ -4,7 +4,7 @@ const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
min-width: 160px;
|
||||
@@ -136,17 +137,21 @@ const Wrapper = styled.div`
|
||||
|
||||
/* Active/selected state - applied to the currently selected item */
|
||||
&.dropdown-item-active {
|
||||
color: ${({ theme }) => theme.colors.text.yellow};
|
||||
background-color: ${({ theme }) => theme.dropdown.activeBg};
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.dropdown.selectedColor} !important;
|
||||
background-color: ${({ theme }) => rgba(theme.dropdown.selectedColor, 0.07)} !important;
|
||||
.dropdown-icon {
|
||||
color: ${({ theme }) => theme.colors.text.yellow};
|
||||
color: ${({ theme }) => theme.dropdown.selectedColor} !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.dropdown.selectedColor} !important;
|
||||
background-color: ${({ theme }) => rgba(theme.dropdown.selectedColor, 0.07)} !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Combined state - when active item is also focused */
|
||||
&.dropdown-item-active.dropdown-item-focused {
|
||||
background-color: ${({ theme }) => theme.dropdown.activeHoverBg};
|
||||
background-color: ${({ theme }) => rgba(theme.dropdown.selectedColor, 0.07)} !important;
|
||||
}
|
||||
|
||||
/* Focus visible for accessibility */
|
||||
|
||||
@@ -9,7 +9,7 @@ const StyledWrapper = styled.div`
|
||||
.table-container {
|
||||
overflow-y: auto;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
border: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border: solid 1px ${(props) => props.theme.border.border0};
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -31,8 +31,8 @@ const StyledWrapper = styled.div`
|
||||
padding: 5px 10px !important;
|
||||
border-top: none !important;
|
||||
border-left: none !important;
|
||||
border-bottom: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-right: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
vertical-align: middle;
|
||||
|
||||
&:last-child {
|
||||
@@ -58,8 +58,8 @@ const StyledWrapper = styled.div`
|
||||
padding: 1px 10px !important;
|
||||
border-top: none !important;
|
||||
border-left: none !important;
|
||||
border-bottom: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-right: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
vertical-align: middle;
|
||||
|
||||
&:last-child {
|
||||
|
||||
@@ -33,7 +33,7 @@ const EnvironmentListContent = ({
|
||||
{environments.map((env) => (
|
||||
<div
|
||||
key={env.uid}
|
||||
className={`dropdown-item ${env.uid === activeEnvironmentUid ? 'active' : ''}`}
|
||||
className={`dropdown-item ${env.uid === activeEnvironmentUid ? 'dropdown-item-active' : ''}`}
|
||||
onClick={() => onEnvironmentSelect(env)}
|
||||
data-tooltip-content={env.name}
|
||||
data-tooltip-hidden={env.name?.length < 90}
|
||||
|
||||
@@ -65,35 +65,6 @@ const Wrapper = styled.div`
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tippy-box .tippy-content {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.6rem;
|
||||
cursor: pointer;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
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;
|
||||
@@ -109,7 +80,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
button {
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -119,7 +90,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
|
||||
.tab-content-wrapper {
|
||||
@@ -170,7 +141,7 @@ const Wrapper = styled.div`
|
||||
min-height: 12.5rem;
|
||||
|
||||
h3 {
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
@@ -178,7 +149,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
p {
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
opacity: 0.75;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
line-height: 1.5;
|
||||
@@ -194,9 +165,9 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.space-y-2 > button {
|
||||
border: 0.0625rem solid ${(props) => props.theme.dropdown.primaryText};
|
||||
border: 0.0625rem solid ${(props) => props.theme.text};
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
width: 100%;
|
||||
@@ -224,7 +195,7 @@ const Wrapper = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
@@ -232,7 +203,7 @@ const Wrapper = styled.div`
|
||||
|
||||
svg {
|
||||
margin: 0 auto 1rem auto;
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const Wrapper = styled.div`
|
||||
.table-container {
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
border: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border: solid 1px ${(props) => props.theme.border.border0};
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -46,8 +46,8 @@ const Wrapper = styled.div`
|
||||
|
||||
td {
|
||||
padding: 5px 10px !important;
|
||||
border-bottom: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-right: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
@@ -64,8 +64,8 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-right: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
@@ -75,26 +75,6 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-param {
|
||||
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: 11px !important;
|
||||
max-width: 200px !important;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
@@ -174,7 +175,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.workspace.environments.activeBg};
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
@@ -235,35 +236,34 @@ const StyledWrapper = styled.div`
|
||||
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;
|
||||
|
||||
.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.colors.text.green};
|
||||
|
||||
&.save {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.listItem.hoverBg};
|
||||
}
|
||||
&:hover {
|
||||
background: ${(props) => rgba(props.theme.colors.text.green, 0.1)};
|
||||
}
|
||||
}
|
||||
|
||||
&.cancel {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
|
||||
&.cancel {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.listItem.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
&:hover {
|
||||
background: ${(props) => rgba(props.theme.colors.text.danger, 0.1)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 10%;
|
||||
flex: 1;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
@@ -32,22 +32,6 @@ const StyledWrapper = styled.div`
|
||||
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 {
|
||||
background: ${(props) => props.theme.listItem.hoverBg};
|
||||
border-color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -5,18 +5,19 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { IconFileAlert } from '@tabler/icons';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import ExportEnvironmentModal from 'components/Environments/Common/ExportEnvironmentModal';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
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')}>
|
||||
<Button size="sm" color="secondary" onClick={() => setTab('create')}>
|
||||
Create Environment
|
||||
</button>
|
||||
<button className="shared-button" onClick={() => setTab('import')}>
|
||||
</Button>
|
||||
<Button size="sm" color="secondary" onClick={() => setTab('import')}>
|
||||
Import Environment
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const StyledWrapper = styled.div`
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -100,15 +100,14 @@ const StyledWrapper = styled.div`
|
||||
.default-badge {
|
||||
padding: 1px 6px;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
background: ${(props) => props.theme.sidebar.badge.bg};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: ${(props) => props.theme.background.surface1};
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.workspace-path {
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
color: ${(props) => props.theme.text.muted};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
color: ${(props) => props.theme.text};
|
||||
@@ -72,7 +73,6 @@ const Wrapper = styled.div`
|
||||
background-color: ${(props) => props.theme.modal.title.bg};
|
||||
font-size: ${(props) => props.theme.font.size.md};
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
border-top-left-radius: ${(props) => props.theme.border.radius.base};
|
||||
border-top-right-radius: ${(props) => props.theme.border.radius.base};
|
||||
|
||||
@@ -98,7 +98,7 @@ const Wrapper = styled.div`
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: ${(props) => props.theme.modal.closeButton.hoverBg};
|
||||
background-color: ${(props) => rgba(props.theme.modal.title.color, 0.1)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import React from 'react';
|
||||
import Font from './Font/index';
|
||||
import Theme from './Theme/index';
|
||||
|
||||
const Display = ({ close }) => {
|
||||
return (
|
||||
<div className="flex flex-col my-2 gap-10 w-full">
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<span>
|
||||
Theme
|
||||
</span>
|
||||
<Theme close={close} />
|
||||
</div>
|
||||
<div className="h-[1px] bg-[#aaa5] w-full"></div>
|
||||
<div className="w-fit flex flex-col gap-2">
|
||||
<Font close={close} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.theme-mode-selector {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.theme-mode-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theme-variant-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.theme-variant-label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.theme-variants {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.theme-variant-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid ${(props) => props.theme.input.border};
|
||||
border-radius: ${(props) => props.theme.border.radius.md};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
min-width: 120px;
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.input.focusBorder};
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: ${(props) => props.theme.accents.primary};
|
||||
background: ${(props) => rgba(props.theme.accents.primary, 0.07)};
|
||||
}
|
||||
}
|
||||
|
||||
.theme-preview {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.theme-preview-sidebar {
|
||||
width: 15px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.theme-preview-main {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.theme-preview-line {
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.theme-variant-name {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: ${(props) => props.theme.input.border};
|
||||
margin: 24px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
130
packages/bruno-app/src/components/Preferences/Themes/index.js
Normal file
130
packages/bruno-app/src/components/Preferences/Themes/index.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { getLightThemes, getDarkThemes } from 'themes/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ThemePreview = ({ themeId, isDark }) => {
|
||||
const bgColor = isDark ? '#1e1e1e' : '#ffffff';
|
||||
const sidebarColor = isDark ? '#252526' : '#f8f8f8';
|
||||
const lineColor = isDark ? '#3d3d3d' : '#e5e5e5';
|
||||
|
||||
return (
|
||||
<div className="theme-preview" style={{ background: bgColor, border: `1px solid ${lineColor}` }}>
|
||||
<div className="theme-preview-sidebar" style={{ background: sidebarColor }} />
|
||||
<div className="theme-preview-main">
|
||||
<div className="theme-preview-line" style={{ background: lineColor }} />
|
||||
<div className="theme-preview-line" style={{ background: lineColor, width: '60%' }} />
|
||||
<div className="theme-preview-line" style={{ background: lineColor, width: '70%' }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ThemeVariantCard = ({ theme, isSelected, onClick }) => {
|
||||
const isDark = theme.mode === 'dark';
|
||||
|
||||
return (
|
||||
<div className={`theme-variant-card ${isSelected ? 'selected' : ''}`} onClick={onClick}>
|
||||
<ThemePreview themeId={theme.id} isDark={isDark} />
|
||||
<span className="theme-variant-name">{theme.name}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Themes = () => {
|
||||
const {
|
||||
storedTheme,
|
||||
setStoredTheme,
|
||||
themeVariantLight,
|
||||
setThemeVariantLight,
|
||||
themeVariantDark,
|
||||
setThemeVariantDark
|
||||
} = useTheme();
|
||||
|
||||
const lightThemes = getLightThemes();
|
||||
const darkThemes = getDarkThemes();
|
||||
|
||||
const handleModeChange = (mode) => {
|
||||
setStoredTheme(mode);
|
||||
};
|
||||
|
||||
const renderThemeVariants = (themes, selectedVariant, onSelect, label) => (
|
||||
<div className="theme-variant-section">
|
||||
<div className="theme-variant-label">{label}</div>
|
||||
<div className="theme-variants">
|
||||
{themes.map((theme) => (
|
||||
<ThemeVariantCard
|
||||
key={theme.id}
|
||||
theme={theme}
|
||||
isSelected={selectedVariant === theme.id}
|
||||
onClick={() => onSelect(theme.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<div>
|
||||
<div className="section-header">Appearance</div>
|
||||
<div className="theme-mode-selector">
|
||||
<label className="theme-mode-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme-mode"
|
||||
value="light"
|
||||
checked={storedTheme === 'light'}
|
||||
onChange={() => handleModeChange('light')}
|
||||
/>
|
||||
<span>Light</span>
|
||||
</label>
|
||||
<label className="theme-mode-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme-mode"
|
||||
value="dark"
|
||||
checked={storedTheme === 'dark'}
|
||||
onChange={() => handleModeChange('dark')}
|
||||
/>
|
||||
<span>Dark</span>
|
||||
</label>
|
||||
<label className="theme-mode-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="theme-mode"
|
||||
value="system"
|
||||
checked={storedTheme === 'system'}
|
||||
onChange={() => handleModeChange('system')}
|
||||
/>
|
||||
<span>System</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{storedTheme === 'light' && (
|
||||
<>
|
||||
{renderThemeVariants(lightThemes, themeVariantLight, setThemeVariantLight, 'Light Theme')}
|
||||
</>
|
||||
)}
|
||||
|
||||
{storedTheme === 'dark' && (
|
||||
<>
|
||||
{renderThemeVariants(darkThemes, themeVariantDark, setThemeVariantDark, 'Dark Theme')}
|
||||
</>
|
||||
)}
|
||||
|
||||
{storedTheme === 'system' && (
|
||||
<>
|
||||
{renderThemeVariants(lightThemes, themeVariantLight, setThemeVariantLight, 'Light Theme')}
|
||||
<div className="section-divider" />
|
||||
{renderThemeVariants(darkThemes, themeVariantDark, setThemeVariantDark, 'Dark Theme')}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Themes;
|
||||
@@ -4,6 +4,7 @@ import React, { useState } from 'react';
|
||||
|
||||
import Support from './Support';
|
||||
import General from './General';
|
||||
import Themes from './Themes';
|
||||
import Proxy from './ProxySettings';
|
||||
import Display from './Display';
|
||||
import Keybindings from './Keybindings';
|
||||
@@ -26,6 +27,10 @@ const Preferences = ({ onClose }) => {
|
||||
return <General close={onClose} />;
|
||||
}
|
||||
|
||||
case 'themes': {
|
||||
return <Themes close={onClose} />;
|
||||
}
|
||||
|
||||
case 'proxy': {
|
||||
return <Proxy close={onClose} />;
|
||||
}
|
||||
@@ -56,6 +61,9 @@ const Preferences = ({ onClose }) => {
|
||||
<div className={getTabClassname('general')} role="tab" onClick={() => setTab('general')}>
|
||||
General
|
||||
</div>
|
||||
<div className={getTabClassname('themes')} role="tab" onClick={() => setTab('themes')}>
|
||||
Themes
|
||||
</div>
|
||||
<div className={getTabClassname('display')} role="tab" onClick={() => setTab('display')}>
|
||||
Display
|
||||
</div>
|
||||
|
||||
@@ -4,55 +4,124 @@ const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
/* height: 100%; */
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.grpc-message-header {
|
||||
.font-medium {
|
||||
color: ${(props) => props.theme.text};
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.single {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
&.multi {
|
||||
overflow-y: auto;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.single {
|
||||
height: 100%;
|
||||
|
||||
.editor-container {
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.single) {
|
||||
min-height: 240px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
padding: 4px 0px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.message-label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
&.delete:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#grpc-messages-container {
|
||||
/* height: 100%; */
|
||||
position: relative;
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.add-message-btn-container {
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
|
||||
p {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-message-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-top: 8px;
|
||||
background: ${(props) => props.theme.bg || '#fff'};
|
||||
z-index: 15;
|
||||
border-top: 1px solid ${(props) => props.theme.border || 'rgba(0, 0, 0, 0.1)'};
|
||||
|
||||
.add-message-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border-top: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
padding: 8px;
|
||||
background: ${(props) => props.theme.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { get } from 'lodash';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
@@ -8,37 +8,85 @@ import { sendGrpcMessage, generateGrpcSampleMessage } from 'utils/network/index'
|
||||
import useLocalStorage from 'hooks/useLocalStorage';
|
||||
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import Button from 'ui/Button';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconSend, IconRefresh, IconWand, IconPlus, IconTrash, IconChevronDown, IconChevronUp } from '@tabler/icons';
|
||||
import { IconSend, IconRefresh, IconWand, IconPlus, IconTrash } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import toast from 'react-hot-toast';
|
||||
import { getAbsoluteFilePath } from 'utils/common/path';
|
||||
import { prettifyJsonString } from 'utils/common/index';
|
||||
|
||||
const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCollapsed, onToggleCollapse, handleRun, canClientSendMultipleMessages }) => {
|
||||
const MessageToolbar = ({
|
||||
index,
|
||||
canClientStream,
|
||||
isConnectionActive,
|
||||
onSend,
|
||||
onRegenerateMessage,
|
||||
onPrettify,
|
||||
onDeleteMessage,
|
||||
showDelete
|
||||
}) => {
|
||||
return (
|
||||
<div className="message-toolbar">
|
||||
<span className="message-label">Message {index + 1}</span>
|
||||
<div className="toolbar-actions mr-2">
|
||||
<ToolHint text="Format JSON" toolhintId={`prettify-msg-${index}`}>
|
||||
<button onClick={onPrettify} className="toolbar-btn">
|
||||
<IconWand size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
<ToolHint text="Generate sample" toolhintId={`regenerate-msg-${index}`}>
|
||||
<button onClick={onRegenerateMessage} className="toolbar-btn">
|
||||
<IconRefresh size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
{canClientStream && (
|
||||
<ToolHint text={isConnectionActive ? 'Send message' : 'Connection not active'} toolhintId={`send-msg-${index}`}>
|
||||
<button
|
||||
onClick={onSend}
|
||||
disabled={!isConnectionActive}
|
||||
className={`toolbar-btn ${!isConnectionActive ? 'disabled' : ''}`}
|
||||
data-testid={`grpc-send-message-${index}`}
|
||||
>
|
||||
<IconSend size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</ToolHint>
|
||||
)}
|
||||
|
||||
{showDelete && (
|
||||
<ToolHint text="Delete message" toolhintId={`delete-msg-${index}`}>
|
||||
<button onClick={onDeleteMessage} className="toolbar-btn delete">
|
||||
<IconTrash size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</ToolHint>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SingleGrpcMessage = ({ message, item, collection, index, methodType, handleRun, canClientSendMultipleMessages, isLast }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme, theme } = useTheme();
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
const isConnectionActive = useSelector((state) => state.collections.activeConnections.includes(item.uid));
|
||||
|
||||
// Access gRPC method metadata from local storage
|
||||
const [reflectionCache] = useLocalStorage('bruno.grpc.reflectionCache', {});
|
||||
const [protofileCache] = useLocalStorage('bruno.grpc.protofileCache', {});
|
||||
|
||||
const canClientStream = methodType === 'client-streaming' || methodType === 'bidi-streaming';
|
||||
|
||||
const { name, content } = message;
|
||||
|
||||
const onEdit = (value) => {
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
|
||||
currentMessages[index] = {
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: value
|
||||
};
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
@@ -53,60 +101,51 @@ const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCol
|
||||
console.error('Error sending message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const onRegenerateMessage = async () => {
|
||||
try {
|
||||
const methodPath = item.draft?.request?.method || item.request?.method;
|
||||
|
||||
if (!methodPath) {
|
||||
toastError(new Error('Method path not found in request'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the URL and protoPath to determine which cache to use
|
||||
const url = item.draft?.request?.url || item.request?.url;
|
||||
const protoPath = item.draft?.request?.protoPath || item.request?.protoPath;
|
||||
|
||||
// Find the method metadata from the appropriate cache
|
||||
let methodMetadata = null;
|
||||
if (protoPath) {
|
||||
// Use protofile cache if protoPath is available
|
||||
const absolutePath = getAbsoluteFilePath(collection.pathname, protoPath);
|
||||
const cachedMethods = protofileCache[absolutePath];
|
||||
if (cachedMethods) {
|
||||
methodMetadata = cachedMethods.find((method) => method.path === methodPath);
|
||||
}
|
||||
} else if (url) {
|
||||
// Use reflection cache if no protoPath (reflection mode)
|
||||
const cachedMethods = reflectionCache[url];
|
||||
if (cachedMethods) {
|
||||
methodMetadata = cachedMethods.find((method) => method.path === methodPath);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await generateGrpcSampleMessage(methodPath,
|
||||
content,
|
||||
{
|
||||
arraySize: 2,
|
||||
methodMetadata // Pass the method metadata to the function
|
||||
});
|
||||
const result = await generateGrpcSampleMessage(methodPath, content, {
|
||||
arraySize: 2,
|
||||
methodMetadata
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
|
||||
currentMessages[index] = {
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
content: result.message
|
||||
};
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
|
||||
toast.success('Sample message generated successfully!');
|
||||
toast.success('Sample message generated');
|
||||
} else {
|
||||
toastError(new Error(result.error || 'Failed to generate sample message'));
|
||||
}
|
||||
@@ -118,9 +157,7 @@ const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCol
|
||||
|
||||
const onDeleteMessage = () => {
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
|
||||
currentMessages.splice(index, 1);
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
@@ -131,7 +168,6 @@ const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCol
|
||||
const onPrettify = () => {
|
||||
try {
|
||||
const prettyBodyJson = prettifyJsonString(content);
|
||||
|
||||
const currentMessages = [...(body.grpc || [])];
|
||||
currentMessages[index] = {
|
||||
name: name ? name : `message ${index + 1}`,
|
||||
@@ -147,101 +183,45 @@ const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCol
|
||||
}
|
||||
};
|
||||
|
||||
const getContainerHeight = (canClientSendMultipleMessages && body.grpc.length > 1) ? `${isCollapsed ? '' : 'h-80'}` : 'h-full';
|
||||
const isSingleMessage = !canClientSendMultipleMessages || body.grpc.length === 1;
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col mb-3 border border-neutral-200 dark:border-neutral-800 rounded-md overflow-hidden ${getContainerHeight} relative`}>
|
||||
<div
|
||||
className="grpc-message-header flex items-center justify-between px-3 py-2 bg-neutral-100 dark:bg-neutral-700 cursor-pointer"
|
||||
onClick={onToggleCollapse}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isCollapsed
|
||||
? <IconChevronDown size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
: <IconChevronUp size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />}
|
||||
<span className="font-medium">{`Message ${canClientStream ? index + 1 : ''}`}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<ToolHint text="Format JSON with proper indentation and spacing" toolhintId={`prettify-msg-${index}`}>
|
||||
<button
|
||||
onClick={onPrettify}
|
||||
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
>
|
||||
<IconWand size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
<ToolHint text="Generate a new sample message based on schema" toolhintId={`regenerate-msg-${index}`}>
|
||||
<button
|
||||
onClick={onRegenerateMessage}
|
||||
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
>
|
||||
<IconRefresh size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
{canClientStream && (
|
||||
<ToolHint text={isConnectionActive ? 'Send gRPC message' : 'Connection not active'} toolhintId={`send-msg-${index}`}>
|
||||
<button
|
||||
onClick={onSend}
|
||||
disabled={!isConnectionActive}
|
||||
className={`p-1 rounded ${isConnectionActive ? 'hover:bg-zinc-200 dark:hover:bg-zinc-600' : 'opacity-50 cursor-not-allowed'} transition-colors`}
|
||||
data-testid={`grpc-send-message-${index}`}
|
||||
>
|
||||
<IconSend
|
||||
size={16}
|
||||
strokeWidth={1.5}
|
||||
className={`${isConnectionActive ? 'text-zinc-700 dark:text-zinc-300' : 'text-zinc-400 dark:text-zinc-500'}`}
|
||||
/>
|
||||
</button>
|
||||
</ToolHint>
|
||||
)}
|
||||
|
||||
{index > 0 && (
|
||||
<ToolHint text="Delete this message" toolhintId={`delete-msg-${index}`}>
|
||||
<button
|
||||
onClick={onDeleteMessage}
|
||||
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
>
|
||||
<IconTrash size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
)}
|
||||
</div>
|
||||
<div className={`message-container ${isSingleMessage ? 'single' : ''} ${isLast ? 'last' : ''}`}>
|
||||
<MessageToolbar
|
||||
index={index}
|
||||
canClientStream={canClientStream}
|
||||
isConnectionActive={isConnectionActive}
|
||||
onSend={onSend}
|
||||
onRegenerateMessage={onRegenerateMessage}
|
||||
onPrettify={onPrettify}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
showDelete={index > 0}
|
||||
/>
|
||||
<div className="editor-container">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={content}
|
||||
onEdit={onEdit}
|
||||
onRun={handleRun}
|
||||
onSave={onSave}
|
||||
mode="application/ld+json"
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className={`flex ${body.grpc.length === 1 || !canClientSendMultipleMessages ? 'h-full' : 'h-80'} relative`}>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={content}
|
||||
onEdit={onEdit}
|
||||
onRun={handleRun}
|
||||
onSave={onSave}
|
||||
mode="application/ld+json"
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GrpcBody = ({ item, collection, handleRun }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
|
||||
const dispatch = useDispatch();
|
||||
const [collapsedMessages, setCollapsedMessages] = useState([]);
|
||||
const messagesContainerRef = useRef(null);
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
|
||||
const methodType = item.draft ? get(item, 'draft.request.methodType') : get(item, 'request.methodType');
|
||||
const canClientSendMultipleMessages = methodType === 'client-streaming' || methodType === 'bidi-streaming';
|
||||
|
||||
// Auto-scroll to the latest message when messages are added
|
||||
useEffect(() => {
|
||||
if (messagesContainerRef.current && body?.grpc?.length > 0) {
|
||||
const container = messagesContainerRef.current;
|
||||
@@ -249,26 +229,12 @@ const GrpcBody = ({ item, collection, handleRun }) => {
|
||||
}
|
||||
}, [body?.grpc?.length]);
|
||||
|
||||
const toggleMessageCollapse = (index) => {
|
||||
setCollapsedMessages((prev) => {
|
||||
if (prev.includes(index)) {
|
||||
return prev.filter((i) => i !== index);
|
||||
} else {
|
||||
return [...prev, index];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addNewMessage = () => {
|
||||
const currentMessages = Array.isArray(body.grpc)
|
||||
? [...body.grpc]
|
||||
: [];
|
||||
|
||||
const currentMessages = Array.isArray(body.grpc) ? [...body.grpc] : [];
|
||||
currentMessages.push({
|
||||
name: `message ${currentMessages.length + 1}`,
|
||||
content: '{}'
|
||||
});
|
||||
|
||||
dispatch(updateRequestBody({
|
||||
content: currentMessages,
|
||||
itemUid: item.uid,
|
||||
@@ -278,61 +244,60 @@ const GrpcBody = ({ item, collection, handleRun }) => {
|
||||
|
||||
if (!body?.grpc || !Array.isArray(body.grpc)) {
|
||||
return (
|
||||
<StyledWrapper isVerticalLayout={isVerticalLayout}>
|
||||
<div className="flex flex-col items-center justify-center py-8">
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mb-4">No gRPC messages available</p>
|
||||
<ToolHint text="Add the first message to your gRPC request" toolhintId="add-first-msg">
|
||||
<button
|
||||
onClick={addNewMessage}
|
||||
className="flex items-center justify-center gap-2 py-2 px-4 rounded-md border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 transition-colors"
|
||||
>
|
||||
<IconPlus size={16} strokeWidth={1.5} className="text-neutral-700 dark:text-neutral-300" />
|
||||
<span className="font-medium text-neutral-700 dark:text-neutral-300">Add First Message</span>
|
||||
</button>
|
||||
</ToolHint>
|
||||
<StyledWrapper>
|
||||
<div className="empty-state">
|
||||
<p>No gRPC messages available</p>
|
||||
<Button
|
||||
onClick={addNewMessage}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon={<IconPlus size={14} strokeWidth={1.5} />}
|
||||
>
|
||||
Add Message
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const messagesToShow = body.grpc.filter((_, index) => canClientSendMultipleMessages || index === 0);
|
||||
|
||||
return (
|
||||
<StyledWrapper isVerticalLayout={isVerticalLayout}>
|
||||
<StyledWrapper>
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
id="grpc-messages-container"
|
||||
data-testid="grpc-messages-container"
|
||||
className={`flex-1 ${body.grpc.length === 1 || !canClientSendMultipleMessages ? 'h-full' : 'overflow-y-auto'} ${canClientSendMultipleMessages && 'pb-16'}`}
|
||||
className={`messages-container ${canClientSendMultipleMessages && messagesToShow.length > 1 ? 'multi' : 'single'}`}
|
||||
>
|
||||
{body.grpc
|
||||
.filter((_, index) => canClientSendMultipleMessages || index === 0)
|
||||
.map((message, index) => (
|
||||
<SingleGrpcMessage
|
||||
key={index}
|
||||
message={message}
|
||||
item={item}
|
||||
collection={collection}
|
||||
index={index}
|
||||
methodType={methodType}
|
||||
isCollapsed={collapsedMessages.includes(index)}
|
||||
onToggleCollapse={() => toggleMessageCollapse(index)}
|
||||
handleRun={handleRun}
|
||||
canClientSendMultipleMessages={canClientSendMultipleMessages}
|
||||
/>
|
||||
))}
|
||||
{messagesToShow.map((message, index) => (
|
||||
<SingleGrpcMessage
|
||||
key={index}
|
||||
message={message}
|
||||
item={item}
|
||||
collection={collection}
|
||||
index={index}
|
||||
methodType={methodType}
|
||||
handleRun={handleRun}
|
||||
canClientSendMultipleMessages={canClientSendMultipleMessages}
|
||||
isLast={index === messagesToShow.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{canClientSendMultipleMessages && (
|
||||
<div className="add-message-btn-container">
|
||||
<ToolHint text="Add a new gRPC message to the request" toolhintId="add-msg-fixed">
|
||||
<button
|
||||
onClick={addNewMessage}
|
||||
className="add-message-btn flex items-center justify-center gap-2 py-2 px-4 rounded-md border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 transition-colors shadow-md"
|
||||
data-testid="grpc-add-message-button"
|
||||
>
|
||||
<IconPlus size={16} strokeWidth={1.5} className="text-neutral-700 dark:text-neutral-300" />
|
||||
<span className="font-medium text-neutral-700 dark:text-neutral-300">Add Message</span>
|
||||
</button>
|
||||
</ToolHint>
|
||||
<div className="add-message-footer">
|
||||
<Button
|
||||
onClick={addNewMessage}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
fullWidth
|
||||
icon={<IconPlus size={14} strokeWidth={1.5} />}
|
||||
data-testid="grpc-add-message-button"
|
||||
>
|
||||
Add Message
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -49,7 +49,7 @@ const Wrapper = styled.div`
|
||||
.infotip-text {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
background-color: ${(props) => props.theme.background.surface2};
|
||||
color: ${(props) => props.theme.text};
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
@@ -72,7 +72,7 @@ const Wrapper = styled.div`
|
||||
margin-left: -4px;
|
||||
border-width: 4px;
|
||||
border-style: solid;
|
||||
border-color: ${(props) => props.theme.requestTabs.active.bg} transparent transparent transparent;
|
||||
border-color: ${(props) => props.theme.background.surface2} transparent transparent transparent;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
|
||||
@@ -348,7 +348,7 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
|
||||
<IconRefresh
|
||||
color={theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
size={20}
|
||||
className={`${(isReflectionMode ? reflectionManagement.isLoadingMethods : protoFileManagement.isLoadingMethods) ? 'animate-spin' : 'cursor-pointer'}`}
|
||||
data-testid="refresh-methods-icon"
|
||||
/>
|
||||
@@ -367,7 +367,7 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
|
||||
<IconCode
|
||||
color={theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
size={20}
|
||||
/>
|
||||
<span className="infotip-text text-xs">Generate grpcurl command</span>
|
||||
</div>
|
||||
@@ -383,7 +383,7 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
|
||||
<IconDeviceFloppy
|
||||
color={item.draft ? theme.colors.text.yellow : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
size={20}
|
||||
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
|
||||
/>
|
||||
<span className="infotip-text text-xs">
|
||||
@@ -394,7 +394,7 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
|
||||
{isConnectionActive && isStreamingMethod && (
|
||||
<div className="connection-controls relative flex items-center h-full gap-3">
|
||||
<div className="infotip" onClick={handleCancelConnection} data-testid="grpc-cancel-connection-button">
|
||||
<IconX color={theme.requestTabs.icon.color} strokeWidth={1.5} size={22} className="cursor-pointer" />
|
||||
<IconX color={theme.requestTabs.icon.color} strokeWidth={1.5} size={20} className="cursor-pointer" />
|
||||
<span className="infotip-text text-xs">Cancel</span>
|
||||
</div>
|
||||
|
||||
@@ -403,7 +403,7 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
|
||||
<IconCheck
|
||||
color={theme.colors.text.green}
|
||||
strokeWidth={2}
|
||||
size={22}
|
||||
size={20}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
@@ -420,7 +420,7 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
|
||||
handleRun(e);
|
||||
}}
|
||||
>
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} />
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={20} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ const StyledWrapper = styled.div`
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
|
||||
@@ -49,7 +49,7 @@ const Wrapper = styled.div`
|
||||
.infotiptext {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
background-color: ${(props) => props.theme.background.surface2};
|
||||
color: ${(props) => props.theme.text};
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
@@ -72,7 +72,7 @@ const Wrapper = styled.div`
|
||||
margin-left: -4px;
|
||||
border-width: 4px;
|
||||
border-style: solid;
|
||||
border-color: ${(props) => props.theme.requestTabs.active.bg} transparent transparent transparent;
|
||||
border-color: ${(props) => props.theme.background.surface2} transparent transparent transparent;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
|
||||
@@ -9,7 +9,7 @@ const Wrapper = styled.div`
|
||||
border-radius: 3px;
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
color: ${(props) => props.theme.brand};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const StyledWrapper = styled.div`
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
|
||||
@@ -17,13 +17,13 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
color: ${(props) => props.theme.brand};
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
fill: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.single {
|
||||
height: 100%;
|
||||
|
||||
.editor-container {
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.single) {
|
||||
min-height: 240px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.message-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
padding: 4px 0px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.message-label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&.delete:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconChevronDown, IconChevronUp, IconTrash, IconWand } from '@tabler/icons';
|
||||
import { IconTrash, IconWand } from '@tabler/icons';
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import { get } from 'lodash';
|
||||
@@ -13,6 +13,7 @@ import { toastError } from 'utils/common/error';
|
||||
import { prettifyJsonString } from 'utils/common/index';
|
||||
import xmlFormat from 'xml-formatter';
|
||||
import WSRequestBodyMode from '../BodyMode/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
export const TYPE_BY_DECODER = {
|
||||
base64: 'binary',
|
||||
@@ -28,10 +29,9 @@ export const SingleWSMessage = ({
|
||||
collection,
|
||||
index,
|
||||
methodType,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
handleRun,
|
||||
canClientSendMultipleMessages
|
||||
canClientSendMultipleMessages,
|
||||
isLast
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme } = useTheme();
|
||||
@@ -88,9 +88,6 @@ export const SingleWSMessage = ({
|
||||
}));
|
||||
};
|
||||
|
||||
const getContainerHeight
|
||||
= canClientSendMultipleMessages && body.ws.length > 1 ? `${isCollapsed ? '' : 'h-80'}` : 'h-full';
|
||||
|
||||
let codeType = messageFormat;
|
||||
if (TYPE_BY_DECODER[type]) {
|
||||
codeType = TYPE_BY_DECODER[type];
|
||||
@@ -144,60 +141,44 @@ export const SingleWSMessage = ({
|
||||
}
|
||||
};
|
||||
|
||||
const isSingleMessage = !canClientSendMultipleMessages || body.ws.length === 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col mb-3 border border-neutral-200 dark:border-neutral-800 rounded-md overflow-hidden ${getContainerHeight} relative`}
|
||||
>
|
||||
<div
|
||||
className="ws-message-header flex items-center justify-between px-3 py-2 bg-neutral-100 dark:bg-neutral-700 cursor-pointer"
|
||||
onClick={onToggleCollapse}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isCollapsed ? (
|
||||
<IconChevronDown size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
) : (
|
||||
<IconChevronUp size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<StyledWrapper className={`message-container ${isSingleMessage ? 'single' : ''} ${isLast ? 'last' : ''}`}>
|
||||
<div className="message-toolbar">
|
||||
<span className="message-label">Message {index + 1}</span>
|
||||
<div className="toolbar-actions">
|
||||
<WSRequestBodyMode mode={messageFormat} onModeChange={onUpdateMessageType} />
|
||||
<ToolHint text="Prettify" toolhintId={`prettify-msg-${index}`}>
|
||||
<button
|
||||
onClick={onPrettify}
|
||||
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
>
|
||||
<IconWand size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
|
||||
<ToolHint text="Format" toolhintId={`prettify-msg-${index}`}>
|
||||
<button onClick={onPrettify} className="toolbar-btn">
|
||||
<IconWand size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
{index > 0 && (
|
||||
<ToolHint text="Delete this message" toolhintId={`delete-msg-${index}`}>
|
||||
<button
|
||||
onClick={onDeleteMessage}
|
||||
className="p-1 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600 transition-colors"
|
||||
>
|
||||
<IconTrash size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
<ToolHint text="Delete message" toolhintId={`delete-msg-${index}`}>
|
||||
<button onClick={onDeleteMessage} className="toolbar-btn delete">
|
||||
<IconTrash size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</ToolHint>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<div className={`flex ${body.ws.length === 1 || !canClientSendMultipleMessages ? 'h-full' : 'h-80'} relative`}>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={content}
|
||||
onEdit={onEdit}
|
||||
onRun={handleRun}
|
||||
onSave={onSave}
|
||||
mode={codemirrorMode[codeType] ?? 'text/plain'}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="editor-container">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={content}
|
||||
onEdit={onEdit}
|
||||
onRun={handleRun}
|
||||
onSave={onSave}
|
||||
mode={codemirrorMode[codeType] ?? 'text/plain'}
|
||||
enableVariableHighlighting={true}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,49 +4,45 @@ const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.ws-message-header {
|
||||
.font-medium {
|
||||
color: ${(props) => props.theme.text};
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.single {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.multi {
|
||||
overflow-y: auto;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-message-btn-container {
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
|
||||
p {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-message-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding-top: 8px;
|
||||
background: ${(props) => props.theme.bg || '#fff'};
|
||||
z-index: 15;
|
||||
border-top: 1px solid ${(props) => props.theme.border || 'rgba(0, 0, 0, 0.1)'};
|
||||
|
||||
.add-message-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border-top: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
padding: 8px;
|
||||
background: ${(props) => props.theme.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { get } from 'lodash';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { IconPlus } from '@tabler/icons';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'ui/Button';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { SingleWSMessage } from './SingleWSMessage/index';
|
||||
|
||||
const WSBody = ({ item, collection, handleRun }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
|
||||
const dispatch = useDispatch();
|
||||
const [collapsedMessages, setCollapsedMessages] = useState([]);
|
||||
const messagesContainerRef = useRef(null);
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
|
||||
@@ -26,16 +23,6 @@ const WSBody = ({ item, collection, handleRun }) => {
|
||||
}
|
||||
}, [body?.ws?.length]);
|
||||
|
||||
const toggleMessageCollapse = (index) => {
|
||||
setCollapsedMessages((prev) => {
|
||||
if (prev.includes(index)) {
|
||||
return prev.filter((i) => i !== index);
|
||||
} else {
|
||||
return [...prev, index];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addNewMessage = () => {
|
||||
const currentMessages = Array.isArray(body.ws) ? [...body.ws] : [];
|
||||
|
||||
@@ -53,60 +40,58 @@ const WSBody = ({ item, collection, handleRun }) => {
|
||||
|
||||
if (!body?.ws || !Array.isArray(body.ws)) {
|
||||
return (
|
||||
<StyledWrapper isVerticalLayout={isVerticalLayout}>
|
||||
<div className="flex flex-col items-center justify-center py-8">
|
||||
<p className="text-zinc-500 dark:text-zinc-400 mb-4">No WebSocket messages available</p>
|
||||
<ToolHint text="Add the first message to your WebSocket request" toolhintId="add-first-msg">
|
||||
<button
|
||||
onClick={addNewMessage}
|
||||
className="flex items-center justify-center gap-2 py-2 px-4 rounded-md border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 transition-colors"
|
||||
>
|
||||
<IconPlus size={16} strokeWidth={1.5} className="text-neutral-700 dark:text-neutral-300" />
|
||||
<span className="font-medium text-neutral-700 dark:text-neutral-300">Add First Message</span>
|
||||
</button>
|
||||
</ToolHint>
|
||||
<StyledWrapper>
|
||||
<div className="empty-state">
|
||||
<p>No WebSocket messages available</p>
|
||||
<Button
|
||||
onClick={addNewMessage}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon={<IconPlus size={14} strokeWidth={1.5} />}
|
||||
>
|
||||
Add Message
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const messagesToShow = body.ws.filter((_, index) => canClientSendMultipleMessages || index === 0);
|
||||
|
||||
return (
|
||||
<StyledWrapper isVerticalLayout={isVerticalLayout}>
|
||||
<StyledWrapper>
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
id="ws-messages-container"
|
||||
className={`flex-1 ${body.ws.length === 1 || !canClientSendMultipleMessages ? 'h-full' : 'overflow-y-auto'} ${canClientSendMultipleMessages && 'pb-16'
|
||||
}`}
|
||||
className={`messages-container ${canClientSendMultipleMessages && messagesToShow.length > 1 ? 'multi' : 'single'}`}
|
||||
>
|
||||
{body.ws
|
||||
.filter((_, index) => canClientSendMultipleMessages || index === 0)
|
||||
.map((message, index) => (
|
||||
<SingleWSMessage
|
||||
key={index}
|
||||
message={message}
|
||||
item={item}
|
||||
collection={collection}
|
||||
index={index}
|
||||
methodType={methodType}
|
||||
isCollapsed={collapsedMessages.includes(index)}
|
||||
onToggleCollapse={() => toggleMessageCollapse(index)}
|
||||
handleRun={handleRun}
|
||||
canClientSendMultipleMessages={canClientSendMultipleMessages}
|
||||
/>
|
||||
))}
|
||||
{messagesToShow.map((message, index) => (
|
||||
<SingleWSMessage
|
||||
key={index}
|
||||
message={message}
|
||||
item={item}
|
||||
collection={collection}
|
||||
index={index}
|
||||
methodType={methodType}
|
||||
handleRun={handleRun}
|
||||
canClientSendMultipleMessages={canClientSendMultipleMessages}
|
||||
isLast={index === messagesToShow.length - 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{canClientSendMultipleMessages && (
|
||||
<div className="add-message-btn-container">
|
||||
<ToolHint text="Add a new WebSocket message to the request" toolhintId="add-msg-fixed">
|
||||
<button
|
||||
onClick={addNewMessage}
|
||||
className="add-message-btn flex items-center justify-center gap-2 py-2 px-4 rounded-md border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 transition-colors shadow-md"
|
||||
>
|
||||
<IconPlus size={16} strokeWidth={1.5} className="text-neutral-700 dark:text-neutral-300" />
|
||||
<span className="font-medium text-neutral-700 dark:text-neutral-300">Add Message</span>
|
||||
</button>
|
||||
</ToolHint>
|
||||
<div className="add-message-footer">
|
||||
<Button
|
||||
onClick={addNewMessage}
|
||||
variant="filled"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
fullWidth
|
||||
icon={<IconPlus size={14} strokeWidth={1.5} />}
|
||||
>
|
||||
Add Message
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -62,7 +62,7 @@ const StyledWrapper = styled.div`
|
||||
.infotip-text {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
background-color: ${(props) => props.theme.background.surface2};
|
||||
color: ${(props) => props.theme.text};
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
@@ -85,7 +85,7 @@ const StyledWrapper = styled.div`
|
||||
margin-left: -4px;
|
||||
border-width: 4px;
|
||||
border-style: solid;
|
||||
border-color: ${(props) => props.theme.requestTabs.active.bg} transparent transparent transparent;
|
||||
border-color: ${(props) => props.theme.background.surface2} transparent transparent transparent;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
@@ -95,7 +95,7 @@ const StyledWrapper = styled.div`
|
||||
.connection-controls {
|
||||
.infotip {
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.errorHoverBg};
|
||||
background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 6%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ const WsQueryUrl = ({ item, collection, handleRun }) => {
|
||||
<IconDeviceFloppy
|
||||
color={hasChanges ? theme.colors.text.yellow : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
size={20}
|
||||
className={`${hasChanges ? 'cursor-pointer' : 'cursor-default'}`}
|
||||
/>
|
||||
<span className="infotip-text text-xs">
|
||||
@@ -164,7 +164,7 @@ const WsQueryUrl = ({ item, collection, handleRun }) => {
|
||||
<IconPlugConnectedX
|
||||
color={theme.colors.text.danger}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
size={20}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<span className="infotip-text text-xs">Close Connection</span>
|
||||
@@ -181,7 +181,7 @@ const WsQueryUrl = ({ item, collection, handleRun }) => {
|
||||
})}
|
||||
color={theme.colors.text.green}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
size={20}
|
||||
/>
|
||||
<span className="infotip-text text-xs">Connect</span>
|
||||
</div>
|
||||
@@ -189,7 +189,7 @@ const WsQueryUrl = ({ item, collection, handleRun }) => {
|
||||
)}
|
||||
|
||||
<div data-testid="run-button" className="cursor-pointer" onClick={handleRunClick}>
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} />
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={20} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,6 @@ import { clearGlobalEnvironmentDraft } from 'providers/ReduxStore/slices/global-
|
||||
import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
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';
|
||||
@@ -28,8 +26,7 @@ import toast from 'react-hot-toast';
|
||||
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid, hasOverflow, setHasOverflow, dropdownContainerRef }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
|
||||
const { theme } = useTheme();
|
||||
const tabNameRef = useRef(null);
|
||||
const tabLabelRef = useRef(null);
|
||||
const lastOverflowStateRef = useRef(null);
|
||||
|
||||
@@ -26,7 +26,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
color: ${(props) => props.theme.brand};
|
||||
}
|
||||
|
||||
&.cursor-default {
|
||||
|
||||
@@ -18,7 +18,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
color: ${(props) => props.theme.brand};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ const StyledWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const StyledWrapper = styled.div`
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
border-left: 4px solid ${(props) => props.theme.colors.text.danger};
|
||||
border-top: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
max-height: 200px;
|
||||
min-height: 70px;
|
||||
border-left: 3px solid ${(props) => props.theme.colors.text.danger};
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
background-color: ${(props) => (props.theme.bg === '#1e1e1e' ? 'rgba(40, 40, 40, 0.5)' : 'rgba(250, 250, 250, 0.9)')};
|
||||
background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 6%, transparent);
|
||||
margin-bottom: 8px;
|
||||
|
||||
.close-button {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.text};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.375rem;
|
||||
margin-bottom: 4px;
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-family: monospace;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
line-height: 1.25rem;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -6,7 +6,7 @@ const GrpcError = ({ error, onClose }) => {
|
||||
if (!error) return null;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-4 mb-2">
|
||||
<StyledWrapper>
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="error-title">gRPC Server Error</div>
|
||||
|
||||
@@ -3,94 +3,79 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-radius: 4px;
|
||||
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
font-family: ${(props) => (props.font === 'default' ? 'monospace' : props.font)};
|
||||
font-size: ${(props) => (props.fontSize ? props.fontSize : '13px')};
|
||||
.empty-state {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.card.bg};
|
||||
.responses-container {
|
||||
height: 100%;
|
||||
|
||||
&.single {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.multi {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.messages-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:not(.last) {
|
||||
border-bottom: 1px solid ${(props) => props.theme.border.border1};
|
||||
}
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
}
|
||||
|
||||
&.open {
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.error-header {
|
||||
background-color: ${(props) => (props.theme.bg === '#1e1e1e' ? 'rgba(185, 28, 28, 0.1)' : '#fee2e2')};
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
.toggle-btn {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stream-status {
|
||||
display: inline-flex;
|
||||
.message-label {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.latest-badge {
|
||||
margin-left: 8px;
|
||||
padding: 2px 6px;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 10%, transparent);
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&.complete {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.cancelled {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
&.streaming {
|
||||
color: ${(props) => props.theme.colors.text.blue};
|
||||
}
|
||||
justify-content: center;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.message-counter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.response-list {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.response-message {
|
||||
.message-content {
|
||||
height: 240px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-panel-background);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,46 +1,31 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Accordion from 'components/Accordion';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { get } from 'lodash';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { formatISO9075 } from 'date-fns';
|
||||
import GrpcError from '../GrpcError';
|
||||
import { IconChevronDown, IconChevronUp } from '@tabler/icons';
|
||||
|
||||
const GrpcQueryResult = ({ item, collection }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(true);
|
||||
const [expandedIndex, setExpandedIndex] = useState(0);
|
||||
|
||||
const response = item.response || {};
|
||||
const responsesList = response?.responses || [];
|
||||
// Reverse the responses list to show the most recent at the top
|
||||
const reversedResponsesList = [...responsesList].reverse();
|
||||
const hasError = response.isError;
|
||||
const hasResponses = responsesList.length > 0;
|
||||
const errorMessage = response.error;
|
||||
|
||||
// Reset error visibility when a new response is received
|
||||
useEffect(() => {
|
||||
if (hasError) {
|
||||
setShowErrorMessage(true);
|
||||
}
|
||||
}, [response, hasError]);
|
||||
|
||||
// Format a timestamp to a human-readable format
|
||||
const formatTimestamp = (timestamp) => {
|
||||
if (!timestamp) return 'Unknown time';
|
||||
|
||||
try {
|
||||
const date = new Date(timestamp);
|
||||
return formatISO9075(date);
|
||||
} catch (e) {
|
||||
return 'Invalid time';
|
||||
}
|
||||
};
|
||||
|
||||
// Format JSON for display
|
||||
const formatJSON = (data) => {
|
||||
try {
|
||||
if (typeof data === 'string') {
|
||||
@@ -55,7 +40,7 @@ const GrpcQueryResult = ({ item, collection }) => {
|
||||
if (!hasResponses && !hasError) {
|
||||
return (
|
||||
<StyledWrapper className="w-full h-full relative flex flex-col">
|
||||
<div className="text-gray-500 dark:text-gray-400 p-4">No messages received</div>
|
||||
<div className="empty-state">No messages received</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
@@ -64,9 +49,8 @@ const GrpcQueryResult = ({ item, collection }) => {
|
||||
<StyledWrapper className="w-full h-full relative flex flex-col mt-2" data-testid="grpc-response-content">
|
||||
{hasError && showErrorMessage && <GrpcError error={errorMessage} onClose={() => setShowErrorMessage(false)} />}
|
||||
{hasResponses && (
|
||||
<div className={`overflow-y-auto ${responsesList.length === 1 ? 'flex-1' : ''}`} data-testid="grpc-responses-container">
|
||||
<div className={`responses-container ${responsesList.length === 1 ? 'single' : 'multi'}`} data-testid="grpc-responses-container">
|
||||
{responsesList.length === 1 ? (
|
||||
// Single message - render directly without accordion
|
||||
<div className="h-full" data-testid="grpc-single-response">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
@@ -79,43 +63,55 @@ const GrpcQueryResult = ({ item, collection }) => {
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Multiple messages - use accordion
|
||||
<Accordion defaultIndex={0} dataTestId="grpc-responses-accordion">
|
||||
{reversedResponsesList.map((response, index) => {
|
||||
// Calculate the original response number (for display purposes)
|
||||
<div className="messages-list" data-testid="grpc-responses-list">
|
||||
{reversedResponsesList.map((resp, index) => {
|
||||
const originalIndex = responsesList.length - index - 1;
|
||||
const isExpanded = expandedIndex === index;
|
||||
|
||||
return (
|
||||
<Accordion.Item key={originalIndex} index={index} data-testid={`grpc-response-item-${originalIndex}`}>
|
||||
<Accordion.Header index={index} style={{ padding: '8px 12px', minHeight: '40px' }}>
|
||||
<div className="flex justify-between w-full">
|
||||
<div className="font-medium">
|
||||
Response {originalIndex + 1} {index === 0 ? '(Latest)' : ''}
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content index={index} style={{ padding: '0px' }}>
|
||||
<div className="h-60">
|
||||
<div
|
||||
key={originalIndex}
|
||||
className={`message-item ${isExpanded ? 'expanded' : 'collapsed'} ${index === reversedResponsesList.length - 1 ? 'last' : ''}`}
|
||||
data-testid={`grpc-response-item-${originalIndex}`}
|
||||
>
|
||||
<div
|
||||
className="message-header"
|
||||
onClick={() => setExpandedIndex(isExpanded ? -1 : index)}
|
||||
>
|
||||
<span className="message-label">
|
||||
Response {originalIndex + 1}
|
||||
{index === 0 && <span className="latest-badge">Latest</span>}
|
||||
</span>
|
||||
<button className="toggle-btn">
|
||||
{isExpanded ? (
|
||||
<IconChevronUp size={16} strokeWidth={1.5} />
|
||||
) : (
|
||||
<IconChevronDown size={16} strokeWidth={1.5} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<div className="message-content">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
value={formatJSON(response)}
|
||||
value={formatJSON(resp)}
|
||||
mode="application/json"
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasError && !hasResponses && !showErrorMessage && (
|
||||
<div className="text-gray-500 dark:text-gray-400 p-4">
|
||||
<div className="empty-state">
|
||||
No messages received. A server error occurred but has been dismissed.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -6,17 +6,24 @@ const StyledWrapper = styled.div`
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
color: #777777;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
padding: 8px 10px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
&.key {
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.value {
|
||||
word-break: break-all;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +32,10 @@ const StyledWrapper = styled.div`
|
||||
background-color: ${(props) => props.theme.table.striped};
|
||||
}
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const GrpcResponseHeaders = ({ metadata }) => {
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="2" className="text-center py-4 text-gray-500">
|
||||
<td colSpan="2" className="text-center py-4 empty-message">
|
||||
No metadata received
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -6,17 +6,24 @@ const StyledWrapper = styled.div`
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
color: #777777;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
padding: 8px 10px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
&.key {
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.value {
|
||||
word-break: break-all;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +32,10 @@ const StyledWrapper = styled.div`
|
||||
background-color: ${(props) => props.theme.table.striped};
|
||||
}
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const ResponseTrailers = ({ trailers }) => {
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan="2" className="text-center py-4 text-gray-500">
|
||||
<td colSpan="2" className="text-center py-4 empty-message">
|
||||
No trailers received
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -12,7 +12,7 @@ const StyledWrapper = styled.div`
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
|
||||
@@ -7,7 +7,7 @@ const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
|
||||
.send-icon {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseSendIcon};
|
||||
color: ${(props) => props.theme.background.surface2};
|
||||
}
|
||||
|
||||
&.vertical-layout {
|
||||
|
||||
@@ -7,7 +7,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.button-dropdown-button {
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
border-color: ${(props) => props.theme.workspace.border};
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
button {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
@@ -18,7 +18,7 @@ const StyledWrapper = styled.div`
|
||||
.cursor-pointer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
|
||||
&:hover {
|
||||
color: var(--color-tab-active);
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
`;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const StyledWrapper = styled.div`
|
||||
padding-top: 20%;
|
||||
width: 100%;
|
||||
.send-icon {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseSendIcon};
|
||||
color: ${(props) => props.theme.background.surface2};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const StyledWrapper = styled.div`
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -12,7 +12,7 @@ const StyledWrapper = styled.div`
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
|
||||
@@ -1,31 +1,59 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
overflow-y: auto;
|
||||
overflow-y: auto;
|
||||
|
||||
.ws-message.new {
|
||||
background-color: ${({ theme }) => theme.table.striped};
|
||||
.empty-state {
|
||||
padding: 1rem;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.ws-message:not(:last-child) {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.table.border};
|
||||
}
|
||||
|
||||
.ws-message:not(:last-child).open {
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
|
||||
.ws-incoming {
|
||||
.ws-message {
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-color: ${(props) => props.theme.table.border};
|
||||
|
||||
&.new {
|
||||
background-color: ${({ theme }) => theme.table.striped};
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.border1};
|
||||
}
|
||||
|
||||
&:not(:last-child).open {
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
.message-timestamp {
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
.ws-outgoing {
|
||||
background: ${(props) => props.theme.bg};
|
||||
border-color: ${(props) => props.theme.table.border};
|
||||
.ws-incoming .message-type-icon {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
.ws-outgoing .message-type-icon {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
.ws-info .message-type-icon {
|
||||
color: ${(props) => props.theme.colors.text.blue};
|
||||
}
|
||||
|
||||
.ws-error .message-type-icon {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -34,11 +62,12 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
div[role='tablist'] {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
.active {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -100,15 +100,17 @@ const WSMessageItem = ({ message, inFocus }) => {
|
||||
}}
|
||||
className={classnames('ws-message flex flex-col p-2', {
|
||||
'ws-incoming': isIncoming,
|
||||
'ws-outgoing': !isIncoming,
|
||||
'ws-outgoing': isOutgoing,
|
||||
'ws-info': isInfo,
|
||||
'ws-error': isError,
|
||||
'open': isOpen,
|
||||
'new': isNew
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames('flex items-center justify-between', {
|
||||
'cursor-pointer': !isInfo,
|
||||
'cursor-not-allowed': isInfo
|
||||
'cursor-pointer': canOpenMessage,
|
||||
'cursor-not-allowed': !canOpenMessage
|
||||
})}
|
||||
onClick={(e) => {
|
||||
if (!canOpenMessage) return;
|
||||
@@ -116,35 +118,26 @@ const WSMessageItem = ({ message, inFocus }) => {
|
||||
}}
|
||||
>
|
||||
<div className="flex min-w-0 shrink">
|
||||
<span
|
||||
className={classnames('font-medium flex items-center gap-1',
|
||||
{
|
||||
'text-green-700': isIncoming,
|
||||
'text-yellow-700': isOutgoing,
|
||||
'text-blue-700': isInfo,
|
||||
'text-red-700': isError,
|
||||
'text-red-700': isError
|
||||
})}
|
||||
>
|
||||
<span className="message-type-icon">
|
||||
<TypeIcon type={message.type} />
|
||||
</span>
|
||||
<span className="ml-3 text-ellipsis max-w-full overflow-hidden text-nowrap">{parsedContent.content}</span>
|
||||
<span className="ml-3 text-ellipsis max-w-full overflow-hidden text-nowrap message-content">{parsedContent.content}</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 gap-2">
|
||||
<div className="flex shrink-0 gap-2 items-center">
|
||||
{message.timestamp && (
|
||||
<span className="text-xs text-gray-400">{new Date(message.timestamp).toISOString()}</span>
|
||||
<span className="message-timestamp">{new Date(message.timestamp).toISOString()}</span>
|
||||
)}
|
||||
{canOpenMessage
|
||||
? (
|
||||
<span className="text-gray-600">
|
||||
<span className="chevron-icon">
|
||||
{isOpen ? (
|
||||
<IconChevronDown size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
<IconChevronDown size={16} strokeWidth={1.5} />
|
||||
) : (
|
||||
<IconChevronRight size={16} strokeWidth={1.5} className="text-zinc-700 dark:text-zinc-300" />
|
||||
<IconChevronRight size={16} strokeWidth={1.5} />
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
: <span class="w-4"></span>}
|
||||
: <span className="w-4"></span>}
|
||||
</div>
|
||||
</div>
|
||||
{isOpen && (
|
||||
@@ -188,11 +181,11 @@ const WSMessageItem = ({ message, inFocus }) => {
|
||||
|
||||
const WSMessagesList = ({ order = -1, messages = [] }) => {
|
||||
if (!messages.length) {
|
||||
return <div className="p-4 text-gray-500">No messages yet.</div>;
|
||||
return <StyledWrapper><div className="empty-state">No messages yet.</div></StyledWrapper>;
|
||||
}
|
||||
const ordered = order === -1 ? messages : messages.slice().reverse();
|
||||
return (
|
||||
<StyledWrapper className="ws-messages-list mt-1 flex flex-col">
|
||||
<StyledWrapper className="ws-messages-list mt-2 flex flex-col">
|
||||
{ordered.map((msg, idx, src) => {
|
||||
const inFocus = order === -1 ? src.length - 1 === idx : idx === 0;
|
||||
return <WSMessageItem key={msg.timestamp} inFocus={inFocus} id={idx} message={msg} />;
|
||||
|
||||
@@ -7,7 +7,7 @@ const StyledWrapper = styled.div`
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
|
||||
@@ -88,7 +88,7 @@ const StyledWrapper = styled.div`
|
||||
transition: transform 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&.is-selected {
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
background-color: ${(props) => props.theme.background.surface2};
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.tabs {
|
||||
@@ -7,7 +8,7 @@ const StyledWrapper = styled.div`
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
@@ -31,14 +32,24 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
padding: 10px;
|
||||
border: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder};
|
||||
background-color: ${(props) => props.theme.sidebar.bg};
|
||||
border: 1px solid ${(props) => props.theme.border.border0};
|
||||
background-color: ${(props) => props.theme.background.base};
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
transition: all 0.1s ease;
|
||||
|
||||
&.no-padding {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.note-warning {
|
||||
color: ${(props) => props.theme.colors.text.warning};
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.warning, 0.06)};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.listItem.hoverBg};
|
||||
background-color: ${(props) => props.theme.background.mantle};
|
||||
border-color: ${(props) => props.theme.border.border2};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -105,7 +105,7 @@ const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`flex !flex-col share-button ${
|
||||
className={`flex !flex-col share-button no-padding ${
|
||||
isCollectionLoading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'cursor-pointer'
|
||||
@@ -113,7 +113,7 @@ const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
onClick={isCollectionLoading ? undefined : handleExportPostmanCollection}
|
||||
>
|
||||
{hasNonExportableRequestTypes.has && (
|
||||
<div className="px-3 py-2 bg-yellow-50 w-full dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 text-xs border-b border-yellow-100 dark:border-yellow-800/20 flex items-center">
|
||||
<div className="px-3 py-2 w-full flex items-center note-warning">
|
||||
<IconAlertTriangle size={16} className="mr-2 flex-shrink-0" />
|
||||
<span>
|
||||
Note:
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.api-specs-badge {
|
||||
margin-inline: 0.5rem;
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 5px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { IconFileCode } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ApiSpecsBadge = () => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="items-center mt-2 relative">
|
||||
<div className="api-specs-badge flex items-center justify-between px-2">
|
||||
<div className="flex items-center py-1 select-none">
|
||||
<span className="mr-2">
|
||||
<IconFileCode size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>APIs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiSpecsBadge;
|
||||
@@ -5,7 +5,7 @@ const StyledWrapper = styled.div`
|
||||
height: 50vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.bg};
|
||||
background-color: ${(props) => props.theme.background.base};
|
||||
|
||||
.code-generator {
|
||||
display: flex;
|
||||
|
||||
@@ -25,7 +25,7 @@ const StyledWrapper = styled.div`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
background-color: ${(props) => props.theme.background.surface2};
|
||||
border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
border-radius: 4px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
@@ -320,12 +320,12 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) =>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<span className="mr-2">
|
||||
<Button type="button" size="sm" color="secondary" variant="ghost" onClick={onClose}>
|
||||
<Button type="button" color="secondary" variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</span>
|
||||
<span>
|
||||
<Button type="submit" size="sm">
|
||||
<Button type="submit">
|
||||
Create
|
||||
</Button>
|
||||
</span>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import styled from 'styled-components';
|
||||
import { darken, rgba } from 'polished';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-group {
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
background-color: ${(props) => props.theme.background.surface1};
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
cursor: pointer;
|
||||
border: 1px solid ${(props) => props.theme.sidebar.badge.border || 'transparent'};
|
||||
border: 1px solid ${(props) => props.theme.background.surface2};
|
||||
}
|
||||
|
||||
.current-group:hover {
|
||||
background-color: ${(props) => props.theme.sidebar.badge.hoverBg || props.theme.sidebar.badge.bg};
|
||||
}
|
||||
background-color: ${(props) => darken(0.03, props.theme.background.surface1)};
|
||||
border-color: ${(props) => darken(0.03, props.theme.background.surface2)};
|
||||
|
||||
/* Fix dropdown positioning */
|
||||
[data-tippy-root] {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.theme-menu {
|
||||
min-width: 160px;
|
||||
padding: 4px 0;
|
||||
background: ${(props) => props.theme.dropdown.bg};
|
||||
border: 1px solid ${(props) => props.theme.dropdown.separator};
|
||||
border-radius: ${(props) => props.theme.border.radius.md};
|
||||
box-shadow: ${(props) => props.theme.dropdown.shadow};
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
padding: 6px 12px 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.07)};
|
||||
}
|
||||
|
||||
&.has-submenu {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item-icon {
|
||||
margin-right: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.menu-item-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
opacity: 0.6;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
height: 1px;
|
||||
background: ${(props) => props.theme.dropdown.separator};
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.submenu {
|
||||
min-width: 180px;
|
||||
padding: 4px 0;
|
||||
background: ${(props) => props.theme.dropdown.bg};
|
||||
border: 1px solid ${(props) => props.theme.dropdown.separator};
|
||||
border-radius: ${(props) => props.theme.border.radius.md};
|
||||
box-shadow: ${(props) => props.theme.dropdown.shadow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,181 @@
|
||||
import React, { useState } from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import { IconChevronRight, IconCheck, IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { getLightThemes, getDarkThemes } from 'themes/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ThemeDropdown = ({ children }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [lightSubmenuOpen, setLightSubmenuOpen] = useState(false);
|
||||
const [darkSubmenuOpen, setDarkSubmenuOpen] = useState(false);
|
||||
const [tooltipEnabled, setTooltipEnabled] = useState(true);
|
||||
|
||||
const {
|
||||
storedTheme,
|
||||
setStoredTheme,
|
||||
themeVariantLight,
|
||||
themeVariantDark,
|
||||
setThemeVariantLight,
|
||||
setThemeVariantDark
|
||||
} = useTheme();
|
||||
|
||||
const lightThemes = getLightThemes();
|
||||
const darkThemes = getDarkThemes();
|
||||
|
||||
const handleModeSelect = (mode) => {
|
||||
setStoredTheme(mode);
|
||||
};
|
||||
|
||||
const handleThemeSelect = (themeId, isLight) => {
|
||||
if (isLight) {
|
||||
setThemeVariantLight(themeId);
|
||||
} else {
|
||||
setThemeVariantDark(themeId);
|
||||
}
|
||||
setIsOpen(false);
|
||||
setLightSubmenuOpen(false);
|
||||
setDarkSubmenuOpen(false);
|
||||
};
|
||||
|
||||
const renderSubmenu = (themes, isLight, currentVariant) => (
|
||||
<div className="submenu">
|
||||
{themes.map((theme) => (
|
||||
<div
|
||||
key={theme.id}
|
||||
className={`menu-item ${currentVariant === theme.id ? 'active' : ''}`}
|
||||
onClick={() => handleThemeSelect(theme.id, isLight)}
|
||||
>
|
||||
<span className="menu-item-label">{theme.name}</span>
|
||||
{currentVariant === theme.id && (
|
||||
<IconCheck size={14} strokeWidth={2} className="check-icon" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const menuContent = (
|
||||
<StyledWrapper>
|
||||
<div className="theme-menu">
|
||||
{/* Mode Section */}
|
||||
<div className="menu-label">Mode</div>
|
||||
<div
|
||||
className={`menu-item ${storedTheme === 'light' ? 'active' : ''}`}
|
||||
onClick={() => handleModeSelect('light')}
|
||||
>
|
||||
<IconSun size={14} strokeWidth={1.5} className="menu-item-icon" />
|
||||
<span className="menu-item-label">Light</span>
|
||||
{storedTheme === 'light' && (
|
||||
<IconCheck size={14} strokeWidth={2} className="check-icon" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`menu-item ${storedTheme === 'dark' ? 'active' : ''}`}
|
||||
onClick={() => handleModeSelect('dark')}
|
||||
>
|
||||
<IconMoon size={14} strokeWidth={1.5} className="menu-item-icon" />
|
||||
<span className="menu-item-label">Dark</span>
|
||||
{storedTheme === 'dark' && (
|
||||
<IconCheck size={14} strokeWidth={2} className="check-icon" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`menu-item ${storedTheme === 'system' ? 'active' : ''}`}
|
||||
onClick={() => handleModeSelect('system')}
|
||||
>
|
||||
<IconDeviceDesktop size={14} strokeWidth={1.5} className="menu-item-icon" />
|
||||
<span className="menu-item-label">System</span>
|
||||
{storedTheme === 'system' && (
|
||||
<IconCheck size={14} strokeWidth={2} className="check-icon" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="menu-divider" />
|
||||
|
||||
{/* Light Themes with Submenu */}
|
||||
<Tippy
|
||||
content={renderSubmenu(lightThemes, true, themeVariantLight)}
|
||||
placement="right-start"
|
||||
interactive={true}
|
||||
arrow={false}
|
||||
offset={[0, 2]}
|
||||
animation={false}
|
||||
visible={lightSubmenuOpen}
|
||||
onClickOutside={() => setLightSubmenuOpen(false)}
|
||||
appendTo="parent"
|
||||
>
|
||||
<div
|
||||
className="menu-item has-submenu"
|
||||
onMouseEnter={() => {
|
||||
setLightSubmenuOpen(true);
|
||||
setDarkSubmenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="menu-item-label">Light Themes</span>
|
||||
<IconChevronRight size={14} strokeWidth={2} className="chevron-icon" />
|
||||
</div>
|
||||
</Tippy>
|
||||
|
||||
{/* Dark Themes with Submenu */}
|
||||
<Tippy
|
||||
content={renderSubmenu(darkThemes, false, themeVariantDark)}
|
||||
placement="right-start"
|
||||
interactive={true}
|
||||
arrow={false}
|
||||
offset={[0, 2]}
|
||||
animation={false}
|
||||
visible={darkSubmenuOpen}
|
||||
onClickOutside={() => setDarkSubmenuOpen(false)}
|
||||
appendTo="parent"
|
||||
>
|
||||
<div
|
||||
className="menu-item has-submenu"
|
||||
onMouseEnter={() => {
|
||||
setDarkSubmenuOpen(true);
|
||||
setLightSubmenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="menu-item-label">Dark Themes</span>
|
||||
<IconChevronRight size={14} strokeWidth={2} className="chevron-icon" />
|
||||
</div>
|
||||
</Tippy>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
const handleOpen = () => {
|
||||
setTooltipEnabled(false);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
setLightSubmenuOpen(false);
|
||||
setDarkSubmenuOpen(false);
|
||||
// Small delay before re-enabling tooltip to prevent flash
|
||||
setTimeout(() => setTooltipEnabled(true), 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolHint text="Theme" toolhintId="ThemeDropdown" place="top" offset={10} hidden={!tooltipEnabled}>
|
||||
<Tippy
|
||||
content={menuContent}
|
||||
placement="top-start"
|
||||
interactive={true}
|
||||
arrow={false}
|
||||
animation={false}
|
||||
visible={isOpen}
|
||||
onClickOutside={handleClose}
|
||||
appendTo="parent"
|
||||
>
|
||||
<div onClick={() => isOpen ? handleClose() : handleOpen()}>
|
||||
{children}
|
||||
</div>
|
||||
</Tippy>
|
||||
</ToolHint>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeDropdown;
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconSettings, IconCookie, IconTool, IconSearch } from '@tabler/icons';
|
||||
import { IconSettings, IconCookie, IconTool, IconSearch, IconPalette } from '@tabler/icons';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { getKeyBindingsForActionAllOS } from 'providers/Hotkeys/keyMappings';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import Preferences from 'components/Preferences';
|
||||
import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
|
||||
import Cookies from 'components/Cookies';
|
||||
import Notifications from 'components/Notifications';
|
||||
import Portal from 'components/Portal';
|
||||
import ThemeDropdown from './ThemeDropdown';
|
||||
import { showPreferences } from 'providers/ReduxStore/slices/app';
|
||||
import { openConsole } from 'providers/ReduxStore/slices/logs';
|
||||
import { useApp } from 'providers/App';
|
||||
@@ -81,6 +81,17 @@ const StatusBar = () => {
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
<ThemeDropdown>
|
||||
<button
|
||||
className="status-bar-button"
|
||||
data-trigger="theme"
|
||||
tabIndex={0}
|
||||
aria-label="Change Theme"
|
||||
>
|
||||
<IconPalette size={16} strokeWidth={1.5} aria-hidden="true" />
|
||||
</button>
|
||||
</ThemeDropdown>
|
||||
|
||||
<ToolHint text="Notifications" toolhintId="Notifications" place="top" offset={10}>
|
||||
<div className="status-bar-button">
|
||||
<Notifications />
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
padding: 0 1.5px;
|
||||
border-radius: 8px;
|
||||
|
||||
// for icon hover
|
||||
position: inherit;
|
||||
|
||||
grid-template-columns: ${({ columns }) =>
|
||||
columns?.[0]?.width
|
||||
? columns.map((col) => `${col?.width}`).join(' ')
|
||||
: columns.map((col) => `${100 / columns.length}%`).join(' ')};
|
||||
}
|
||||
|
||||
table thead,
|
||||
table tbody,
|
||||
table tr {
|
||||
display: contents;
|
||||
border: none;
|
||||
}
|
||||
|
||||
table th {
|
||||
position: relative;
|
||||
border: none;
|
||||
border-bottom: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
background: ${(props) => props.theme.examples.table.thead.bg};
|
||||
color: ${(props) => props.theme.examples.table.thead.color};
|
||||
font-weight: 400;
|
||||
|
||||
tr td {
|
||||
border: none;
|
||||
color: ${(props) => props.theme.examples.table.thead.color};
|
||||
background: ${(props) => props.theme.examples.table.thead.bg};
|
||||
}
|
||||
}
|
||||
|
||||
table tr td {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
}
|
||||
|
||||
tr {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
tr.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
tr.hovered {
|
||||
transform: translateY(10px); /* Adjust the value as needed for the animation effect */
|
||||
}
|
||||
|
||||
table tr th {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
font-weight: 400;
|
||||
|
||||
&:nth-child(1) {
|
||||
border-left: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,109 +0,0 @@
|
||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Table = ({ minColumnWidth = 1, headers = [], children }) => {
|
||||
const [activeColumnIndex, setActiveColumnIndex] = useState(null);
|
||||
const tableRef = useRef(null);
|
||||
|
||||
const columns = headers?.map((item) => ({
|
||||
...item,
|
||||
ref: useRef()
|
||||
}));
|
||||
|
||||
const updateDivHeights = () => {
|
||||
if (tableRef.current) {
|
||||
const height = tableRef.current.offsetHeight;
|
||||
columns.forEach((col) => {
|
||||
if (col.ref.current) {
|
||||
col.ref.current.querySelector('.resizer').style.height = `${height}px`;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateDivHeights();
|
||||
window.addEventListener('resize', updateDivHeights);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateDivHeights);
|
||||
};
|
||||
}, [columns]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableRef.current) {
|
||||
const observer = new MutationObserver(updateDivHeights);
|
||||
observer.observe(tableRef.current, { childList: true, subtree: true });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}
|
||||
}, [columns]);
|
||||
|
||||
const handleMouseDown = (index) => (e) => {
|
||||
setActiveColumnIndex(index);
|
||||
};
|
||||
|
||||
const handleMouseMove = useCallback((e) => {
|
||||
const gridColumns = columns.map((col, i) => {
|
||||
if (i === activeColumnIndex) {
|
||||
const width = e.clientX - col.ref?.current?.getBoundingClientRect()?.left;
|
||||
|
||||
if (width >= minColumnWidth) {
|
||||
return `${width}px`;
|
||||
}
|
||||
}
|
||||
return `${col.ref.current.offsetWidth}px`;
|
||||
});
|
||||
|
||||
tableRef.current.style.gridTemplateColumns = `${gridColumns.join(' ')}`;
|
||||
},
|
||||
[activeColumnIndex, columns, minColumnWidth]);
|
||||
|
||||
const removeListeners = useCallback(() => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', removeListeners);
|
||||
}, [handleMouseMove]);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setActiveColumnIndex(null);
|
||||
removeListeners?.();
|
||||
}, [removeListeners]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeColumnIndex !== null) {
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
return () => {
|
||||
removeListeners();
|
||||
};
|
||||
}, [activeColumnIndex, handleMouseMove, handleMouseUp, removeListeners]);
|
||||
|
||||
return (
|
||||
<StyledWrapper columns={columns}>
|
||||
<div className="relative">
|
||||
<table ref={tableRef} className="inherit">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(({ ref, name }, i) => (
|
||||
<th ref={ref} key={name} title={name}>
|
||||
<span>{name}</span>
|
||||
<div
|
||||
className="resizer absolute cursor-col-resize w-[4px] right-[-2px] top-0 z-10 opacity-50 hover:bg-blue-500 active:bg-blue-500"
|
||||
onMouseDown={handleMouseDown(i)}
|
||||
>
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
@@ -25,14 +25,14 @@ const StyledWrapper = styled.div`
|
||||
|
||||
table th {
|
||||
position: relative;
|
||||
border-bottom: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
border-bottom: 1px solid ${(props) => props.theme.border.BORDER0};
|
||||
}
|
||||
|
||||
table tr td {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
border-top: 1px solid ${(props) => props.theme.border.BORDER0};
|
||||
border-right: 1px solid ${(props) => props.theme.border.BORDER0};
|
||||
}
|
||||
|
||||
tr {
|
||||
@@ -50,11 +50,11 @@ const StyledWrapper = styled.div`
|
||||
table tr th {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
border-top: 1px solid ${(props) => props.theme.border.BORDER0};
|
||||
border-right: 1px solid ${(props) => props.theme.border.BORDER0};
|
||||
|
||||
&:nth-child(1) {
|
||||
border-left: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
border-left: 1px solid ${(props) => props.theme.border.BORDER0};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -26,8 +26,8 @@ const StyledWrapper = styled.div`
|
||||
cursor: default;
|
||||
|
||||
&:has(.tag-remove:hover) {
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
border-color: ${(props) => props.theme.requestTabs.active.border || props.theme.requestTabs.bottomBorder};
|
||||
background-color: ${(props) => props.theme.background.surface2};
|
||||
border-color: ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background-color: ${(props) => props.theme.sidebar.badge};
|
||||
color: ${(props) => props.theme.text};
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Tooltip as ReactToolHint } from 'react-tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const ToolHint = ({
|
||||
@@ -20,8 +19,8 @@ const ToolHint = ({
|
||||
const { theme: contextTheme } = useTheme();
|
||||
const appliedTheme = theme || contextTheme;
|
||||
|
||||
const toolhintBackgroundColor = appliedTheme?.sidebar.badge.bg || 'black';
|
||||
const toolhintTextColor = appliedTheme?.text || 'white';
|
||||
const toolhintBackgroundColor = appliedTheme?.background.surface1;
|
||||
const toolhintTextColor = appliedTheme?.text;
|
||||
|
||||
const combinedToolhintStyle = {
|
||||
...tooltipStyle,
|
||||
@@ -40,21 +39,19 @@ const ToolHint = ({
|
||||
<>
|
||||
{!anchorSelect && <span id={toolhintId} className={className}>{children}</span>}
|
||||
{anchorSelect && children}
|
||||
|
||||
<StyledWrapper theme={appliedTheme}>
|
||||
<ReactToolHint
|
||||
{...toolhintProps_final}
|
||||
content={anchorSelect ? undefined : text}
|
||||
className="toolhint"
|
||||
offset={offset}
|
||||
place={place}
|
||||
hidden={hidden}
|
||||
positionStrategy={positionStrategy}
|
||||
noArrow={true}
|
||||
delayShow={delayShow}
|
||||
style={combinedToolhintStyle}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
<ReactToolHint
|
||||
{...toolhintProps_final}
|
||||
content={anchorSelect ? undefined : text}
|
||||
className="toolhint"
|
||||
offset={offset}
|
||||
place={place}
|
||||
hidden={hidden}
|
||||
positionStrategy={positionStrategy}
|
||||
noArrow={true}
|
||||
delayShow={delayShow}
|
||||
style={combinedToolhintStyle}
|
||||
opacity={1}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -88,8 +88,8 @@ const VariablesEditor = ({ collection }) => {
|
||||
|
||||
const reactInspectorTheme
|
||||
= displayedTheme === 'light'
|
||||
? { ...chromeLight, OBJECT_VALUE_STRING_COLOR: theme.variables.runtime.color }
|
||||
: { ...chromeDark, OBJECT_VALUE_STRING_COLOR: theme.variables.runtime.color };
|
||||
? { ...chromeLight, OBJECT_VALUE_STRING_COLOR: theme.text.base }
|
||||
: { ...chromeDark, OBJECT_VALUE_STRING_COLOR: theme.text.base };
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 py-4 overflow-auto">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.main-content {
|
||||
@@ -67,7 +68,7 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.colors.text.green}1A;
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.green, 0.1)};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +76,7 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.colors.text.danger}1A;
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.danger, 0.1)};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,21 +94,6 @@ const StyledWrapper = styled.div`
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.listItem.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -88,7 +88,7 @@ const StyledWrapper = styled.div`
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 8px;
|
||||
background: ${(props) => props.theme.workspace.card.bg};
|
||||
background: ${(props) => props.theme.background.mantle};
|
||||
border: 1px solid ${(props) => props.theme.workspace.border};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
margin-bottom: 16px;
|
||||
|
||||
@@ -8,6 +8,7 @@ import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconFileText, IconEdit, IconX } from '@tabler/icons';
|
||||
import Button from 'ui/Button';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const WorkspaceDocs = ({ workspace }) => {
|
||||
@@ -113,9 +114,9 @@ const WorkspaceDocs = ({ workspace }) => {
|
||||
<li>Key workflows</li>
|
||||
<li>Resources & FAQs</li>
|
||||
</ul>
|
||||
<button className="add-docs-btn" onClick={handleAddDocumentation}>
|
||||
<Button color="secondary" size="md" onClick={handleAddDocumentation}>
|
||||
Add Documentation
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ const Wrapper = styled.div`
|
||||
.table-container {
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
border: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border: solid 1px ${(props) => props.theme.border.border0};
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -45,8 +45,8 @@ const Wrapper = styled.div`
|
||||
|
||||
td {
|
||||
padding: 5px 10px !important;
|
||||
border-bottom: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-right: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
@@ -63,8 +63,8 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-right: ${(props) => props.theme.workspace.environments.indentBorder};
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
@@ -174,7 +175,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.workspace.environments.activeBg};
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
@@ -250,19 +251,18 @@ const StyledWrapper = styled.div`
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&.save {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.listItem.hoverBg};
|
||||
background: ${(props) => rgba(props.theme.colors.text.green, 0.1)};
|
||||
}
|
||||
}
|
||||
|
||||
&.cancel {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.listItem.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
background: ${(props) => rgba(props.theme.colors.text.danger, 0.1)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
padding-top: 10%;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
svg {
|
||||
@@ -32,22 +31,6 @@ const StyledWrapper = styled.div`
|
||||
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 {
|
||||
background: ${(props) => props.theme.listItem.hoverBg};
|
||||
border-color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -6,18 +6,19 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { IconFileAlert } from '@tabler/icons';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import ExportEnvironmentModal from 'components/Environments/Common/ExportEnvironmentModal';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
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')}>
|
||||
<Button size="sm" color="secondary" onClick={() => setTab('create')}>
|
||||
Create Environment
|
||||
</button>
|
||||
<button className="shared-button" onClick={() => setTab('import')}>
|
||||
</Button>
|
||||
<Button size="sm" color="secondary" onClick={() => setTab('import')}>
|
||||
Import Environment
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -97,25 +97,6 @@ const StyledWrapper = styled.div`
|
||||
.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;
|
||||
|
||||
@@ -288,7 +288,7 @@ const CollectionsList = ({ workspace }) => {
|
||||
<span>Share</span>
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item dropdown-item-danger"
|
||||
className="dropdown-item delete-item"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemoveCollection(collection);
|
||||
|
||||
@@ -6,6 +6,7 @@ import toast from 'react-hot-toast';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
|
||||
import Button from 'ui/Button';
|
||||
import CollectionsList from './CollectionsList';
|
||||
import WorkspaceDocs from '../WorkspaceDocs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -107,18 +108,30 @@ const WorkspaceOverview = ({ workspace }) => {
|
||||
<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}>
|
||||
<IconDownload size={14} strokeWidth={1.5} />
|
||||
<span>Import Collection</span>
|
||||
</button>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon={<IconPlus size={14} strokeWidth={1.5} />}
|
||||
onClick={handleCreateCollection}
|
||||
>
|
||||
Create Collection
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon={<IconFolder size={14} strokeWidth={1.5} />}
|
||||
onClick={handleOpenCollection}
|
||||
>
|
||||
Open Collection
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
icon={<IconDownload size={14} strokeWidth={1.5} />}
|
||||
onClick={handleImportCollection}
|
||||
>
|
||||
Import Collection
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Validator } from 'jsonschema';
|
||||
import toast from 'react-hot-toast';
|
||||
import themes from 'themes/index';
|
||||
import themeSchema from 'themes/schema';
|
||||
import useLocalStorage from 'hooks/useLocalStorage/index';
|
||||
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { createContext, useContext, useEffect, useState, useMemo } from 'react';
|
||||
import { ThemeProvider as SCThemeProvider } from 'styled-components';
|
||||
|
||||
const validator = new Validator();
|
||||
|
||||
export const ThemeContext = createContext();
|
||||
export const ThemeProvider = (props) => {
|
||||
const isBrowserThemeLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
const [displayedTheme, setDisplayedTheme] = useState(isBrowserThemeLight ? 'light' : 'dark');
|
||||
const [storedTheme, setStoredTheme] = useLocalStorage('bruno.theme', 'system');
|
||||
const [themeVariantLight, setThemeVariantLight] = useLocalStorage('bruno.themeVariantLight', 'light');
|
||||
const [themeVariantDark, setThemeVariantDark] = useLocalStorage('bruno.themeVariantDark', 'dark');
|
||||
const toggleHtml = () => {
|
||||
const html = document.querySelector('html');
|
||||
if (html) {
|
||||
@@ -45,14 +52,50 @@ export const ThemeProvider = (props) => {
|
||||
// storedTheme can have 3 values: 'light', 'dark', 'system'
|
||||
// displayedTheme can have 2 values: 'light', 'dark'
|
||||
|
||||
const theme = storedTheme === 'system' ? themes[displayedTheme] : themes[storedTheme];
|
||||
const themeOptions = Object.keys(themes);
|
||||
// Get the appropriate variant based on the current display mode
|
||||
const theme = useMemo(() => {
|
||||
const isLightMode = displayedTheme === 'light';
|
||||
const variantName = isLightMode ? themeVariantLight : themeVariantDark;
|
||||
const fallbackTheme = isLightMode ? themes.light : themes.dark;
|
||||
const fallbackName = isLightMode ? 'light' : 'dark';
|
||||
|
||||
// Check if the variant exists in themes
|
||||
const selectedTheme = themes[variantName];
|
||||
if (!selectedTheme) {
|
||||
// Only show toast if using a non-default variant that doesn't exist
|
||||
if (variantName !== fallbackName) {
|
||||
toast.error(`Theme "${variantName}" not found. Using default ${fallbackName} theme.`, {
|
||||
duration: 4000,
|
||||
id: `theme-not-found-${variantName}` // Prevent duplicate toasts
|
||||
});
|
||||
}
|
||||
return fallbackTheme;
|
||||
}
|
||||
|
||||
// Validate the theme against the schema
|
||||
const validationResult = validator.validate(selectedTheme, themeSchema);
|
||||
if (!validationResult.valid) {
|
||||
const errors = validationResult.errors?.map((e) => e.stack).join(', ') || 'Unknown validation error';
|
||||
console.error(`Theme "${variantName}" validation failed:`, errors);
|
||||
toast.error(`Invalid theme "${variantName}". Using default ${fallbackName} theme.`, {
|
||||
duration: 4000,
|
||||
id: `theme-invalid-${variantName}` // Prevent duplicate toasts
|
||||
});
|
||||
return fallbackTheme;
|
||||
}
|
||||
|
||||
return selectedTheme;
|
||||
}, [displayedTheme, themeVariantLight, themeVariantDark]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
themeOptions,
|
||||
storedTheme,
|
||||
displayedTheme,
|
||||
setStoredTheme
|
||||
setStoredTheme,
|
||||
themeVariantLight,
|
||||
setThemeVariantLight,
|
||||
themeVariantDark,
|
||||
setThemeVariantDark
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import 'buttons';
|
||||
@@ -12,7 +12,6 @@
|
||||
--color-tab-inactive: rgb(155 155 155);
|
||||
--color-tab-active-border: #546de5;
|
||||
--color-layout-border: #dedede;
|
||||
--color-text-link: #1663bb;
|
||||
--color-text-danger: rgb(185, 28, 28);
|
||||
--color-background-danger: #dc3545;
|
||||
--color-method-get: rgb(5, 150, 105);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user