mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
11 Commits
chore/desi
...
workspaces
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae8851385f | ||
|
|
77d2fecfe6 | ||
|
|
663b06d60f | ||
|
|
dc3b074520 | ||
|
|
c7be4775b3 | ||
|
|
72d5411df8 | ||
|
|
d167be658f | ||
|
|
08c183b4ec | ||
|
|
c8d13f16c3 | ||
|
|
399201bbc9 | ||
|
|
93eae99302 |
@@ -6,7 +6,7 @@
|
||||
|
||||
- Use 2 spaces for indentation. No tabs, just spaces – keeps everything neat and uniform.
|
||||
|
||||
- Stick to single quotes for strings. For JSX/TSX attributes, use double quotes (e.g., <svg xmlns="..." viewBox="...">) to follow React conventions.
|
||||
- Stick to single quotes for strings. Double quotes are cool elsewhere, but here we go single.
|
||||
|
||||
- Always add semicolons at the end of statements. It's like putting a period at the end of a sentence – clarity matters.
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
'**/dist/**/*',
|
||||
'**/*.bru',
|
||||
'packages/bruno-js/src/sandbox/bundle-browser-rollup.js',
|
||||
'packages/bruno-app/public/static/**/*',
|
||||
'packages/bruno-app/.next/**/*',
|
||||
'packages/bruno-electron/web/**/*'
|
||||
'packages/bruno-app/public/static/**/*'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
1977
package-lock.json
generated
1977
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -45,8 +45,6 @@
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"jsesc": "^3.0.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"jshint": "^2.13.6",
|
||||
"json5": "^2.2.3",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
@@ -85,7 +83,6 @@
|
||||
"shell-quote": "^1.8.3",
|
||||
"strip-json-comments": "^5.0.1",
|
||||
"styled-components": "^5.3.3",
|
||||
"swagger-ui-react": "5.17.12",
|
||||
"system": "^2.0.1",
|
||||
"url": "^0.11.3",
|
||||
"xml-formatter": "^3.5.0",
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
const yamlPlugin = (cm) => {
|
||||
cm.defineMode('yaml', function () {
|
||||
var cons = ['true', 'false', 'on', 'off', 'yes', 'no'];
|
||||
var keywordRegex = new RegExp('\\b((' + cons.join(')|(') + '))$', 'i');
|
||||
|
||||
return {
|
||||
token: function (stream, state) {
|
||||
var ch = stream.peek();
|
||||
var esc = state.escaped;
|
||||
state.escaped = false;
|
||||
/* comments */
|
||||
if (ch == '#' && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) {
|
||||
stream.skipToEnd();
|
||||
return 'comment';
|
||||
}
|
||||
|
||||
if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/)) return 'string';
|
||||
|
||||
if (state.literal && stream.indentation() > state.keyCol) {
|
||||
stream.skipToEnd();
|
||||
return 'string';
|
||||
} else if (state.literal) {
|
||||
state.literal = false;
|
||||
}
|
||||
if (stream.sol()) {
|
||||
state.keyCol = 0;
|
||||
state.pair = false;
|
||||
state.pairStart = false;
|
||||
/* document start */
|
||||
if (stream.match('---')) {
|
||||
return 'def';
|
||||
}
|
||||
/* document end */
|
||||
if (stream.match('...')) {
|
||||
return 'def';
|
||||
}
|
||||
/* array list item */
|
||||
if (stream.match(/\s*-\s+/)) {
|
||||
return 'meta';
|
||||
}
|
||||
}
|
||||
/* inline pairs/lists */
|
||||
if (stream.match(/^(\{|\}|\[|\])/)) {
|
||||
if (ch == '{') state.inlinePairs++;
|
||||
else if (ch == '}') state.inlinePairs--;
|
||||
else if (ch == '[') state.inlineList++;
|
||||
else state.inlineList--;
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* list separator */
|
||||
if (state.inlineList > 0 && !esc && ch == ',') {
|
||||
stream.next();
|
||||
return 'meta';
|
||||
}
|
||||
/* pairs separator */
|
||||
if (state.inlinePairs > 0 && !esc && ch == ',') {
|
||||
state.keyCol = 0;
|
||||
state.pair = false;
|
||||
state.pairStart = false;
|
||||
stream.next();
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* start of value of a pair */
|
||||
if (state.pairStart) {
|
||||
/* block literals */
|
||||
if (stream.match(/^\s*(\||\>)\s*/)) {
|
||||
state.literal = true;
|
||||
return 'meta';
|
||||
}
|
||||
/* references */
|
||||
if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) {
|
||||
return 'variable-2';
|
||||
}
|
||||
/* numbers */
|
||||
if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) {
|
||||
return 'number';
|
||||
}
|
||||
if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) {
|
||||
return 'number';
|
||||
}
|
||||
/* keywords */
|
||||
if (stream.match(keywordRegex)) {
|
||||
return 'keyword';
|
||||
}
|
||||
}
|
||||
|
||||
/* pairs (associative arrays) -> key */
|
||||
if (
|
||||
!state.pair
|
||||
&& stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/)
|
||||
) {
|
||||
state.pair = true;
|
||||
state.keyCol = stream.indentation();
|
||||
return 'atom';
|
||||
}
|
||||
if (state.pair && stream.match(/^:\s*/)) {
|
||||
state.pairStart = true;
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* nothing found, continue */
|
||||
state.pairStart = false;
|
||||
state.escaped = ch == '\\';
|
||||
stream.next();
|
||||
return null;
|
||||
},
|
||||
startState: function () {
|
||||
return {
|
||||
pair: false,
|
||||
pairStart: false,
|
||||
keyCol: 0,
|
||||
inlinePairs: 0,
|
||||
inlineList: 0,
|
||||
literal: false,
|
||||
escaped: false
|
||||
};
|
||||
},
|
||||
lineComment: '#',
|
||||
fold: 'indent'
|
||||
};
|
||||
});
|
||||
|
||||
cm.defineMIME('text/x-yaml', 'yaml');
|
||||
cm.defineMIME('text/yaml', 'yaml');
|
||||
};
|
||||
|
||||
export default yamlPlugin;
|
||||
@@ -1,65 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
height: calc(100vh - 4rem);
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
font-family: ${(props) => (props.font ? props.font : 'default')};
|
||||
line-break: anywhere;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog {
|
||||
overflow: visible;
|
||||
input {
|
||||
background: transparent;
|
||||
border: 1px solid #d3d6db;
|
||||
outline: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
.CodeMirror-overlayscroll-vertical div {
|
||||
background: #d2d7db;
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Todo: dark mode temporary fix
|
||||
// Clean this
|
||||
.CodeMirror.cm-s-monokai {
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
.CodeMirror-overlayscroll-vertical div {
|
||||
background: #444444;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property,
|
||||
.cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-string {
|
||||
color: #ce9178 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-number {
|
||||
color: #b5cea8 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-atom {
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
|
||||
.cm-variable-valid {
|
||||
color: green;
|
||||
}
|
||||
.cm-variable-invalid {
|
||||
color: red;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,138 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import yamlPlugin from './Plugins/Yaml/index';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
this.lintOptions = {
|
||||
esversion: 11,
|
||||
expr: true,
|
||||
asi: true
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
switch (this.props.mode) {
|
||||
case 'yaml':
|
||||
// YAML linting and hightlighting plugin
|
||||
yamlPlugin(CodeMirror);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const editor = (this.editor = CodeMirror(this._node, {
|
||||
value: this.props.value || '',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
tabSize: 2,
|
||||
mode: this.props.mode || 'application/text',
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
showCursorWhenSelecting: true,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||
lint: this.lintOptions,
|
||||
readOnly: this.props.readOnly,
|
||||
scrollbarStyle: 'overlay',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
extraKeys: {
|
||||
'Cmd-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Ctrl-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Cmd-F': 'findPersistent',
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
'Tab': function (cm) {
|
||||
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
|
||||
? cm.execCommand('indentMore')
|
||||
: cm.replaceSelection(' ', 'end');
|
||||
},
|
||||
'Shift-Tab': 'indentLess',
|
||||
'Ctrl-Space': 'autocomplete',
|
||||
'Cmd-Space': 'autocomplete',
|
||||
'Ctrl-Y': 'foldAll',
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
'Cmd-I': 'unfoldAll'
|
||||
}
|
||||
}));
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this.ignoreChangeEvent = true;
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
}
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.editor) {
|
||||
this.editor.refresh();
|
||||
}
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full w-full graphiql-container"
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
this.cachedValue = this.editor.getValue();
|
||||
if (this.props.onEdit) {
|
||||
this.props.onEdit(this.cachedValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from './CodeEditor/index';
|
||||
import { IconDeviceFloppy } from '@tabler/icons';
|
||||
import { saveApiSpecToFile } from 'providers/ReduxStore/slices/apiSpec';
|
||||
import { useState } from 'react';
|
||||
|
||||
const FileEditor = ({ apiSpec }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme, theme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const [content, setContent] = useState(apiSpec?.raw);
|
||||
|
||||
const onEdit = (value) => {
|
||||
setContent(value);
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
dispatch(saveApiSpecToFile({ uid: apiSpec?.uid, content }));
|
||||
};
|
||||
|
||||
const hasChanges = Boolean(content != apiSpec?.raw);
|
||||
|
||||
const editorMode = 'yaml';
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow relative">
|
||||
<CodeEditor
|
||||
theme={displayedTheme}
|
||||
value={content}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
mode={editorMode}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
<IconDeviceFloppy
|
||||
onClick={onSave}
|
||||
color={hasChanges ? theme.colors.text.yellow : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className={`absolute right-0 top-0 m-4 ${
|
||||
hasChanges ? 'cursor-pointer oapcity-100' : 'cursor-default opacity-50'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileEditor;
|
||||
@@ -1,19 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.swagger-root {
|
||||
height: calc(100vh - 4rem);
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
|
||||
&.dark {
|
||||
.swagger-ui {
|
||||
filter: invert(88%) hue-rotate(180deg);
|
||||
}
|
||||
.swagger-ui .microlight {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,19 +0,0 @@
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const Swagger = ({ string }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
|
||||
console.log('string', string);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className={`swagger-root w-full overflow-y-scroll ${displayedTheme}`}>
|
||||
<SwaggerUI spec={string} />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Swagger;
|
||||
@@ -1,22 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.menu-icon {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
}
|
||||
|
||||
div.dropdown-item.menu-item {
|
||||
color: ${(props) => props.theme.colors.danger};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.colors.bg.danger};
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.react-tooltip {
|
||||
z-index: 10;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, { forwardRef, useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconFileCode, IconDots } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FileEditor from './FileEditor';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { openApiSpec } from 'providers/ReduxStore/slices/apiSpec';
|
||||
import { useState } from 'react';
|
||||
import CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec';
|
||||
import { Suspense } from 'react';
|
||||
import Swagger from './Renderers/Swagger';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const ApiSpecPanel = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [createApiSpecModalOpen, setCreateApiSpecModalOpen] = useState(false);
|
||||
|
||||
const { apiSpecs, activeApiSpecUid } = useSelector((state) => state.apiSpec);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
let apiSpec = find(apiSpecs, (c) => c.uid === activeApiSpecUid);
|
||||
const { filename, pathname, raw, uid } = apiSpec || {};
|
||||
if (!uid) {
|
||||
return <div className="p-4 opacity-50">API Spec not found!</div>;
|
||||
}
|
||||
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<IconDots size={22} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleOpenApiSpec = () => {
|
||||
dispatch(openApiSpec()).catch(
|
||||
(err) => console.log(err) && toast.error('An error occurred while opening the API spec')
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col flex-grow relative">
|
||||
{createApiSpecModalOpen ? <CreateApiSpec onClose={() => setCreateApiSpecModalOpen(false)} /> : null}
|
||||
<div className="p-3 mb-2 w-full flex flex-row justify-between grid grid-cols-3">
|
||||
<div className="flex flex-row justify-start gap-x-4 col-span-1">
|
||||
<div className="flex w-fit items-center cursor-pointer">
|
||||
<IconFileCode size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2 mr-4 font-semibold">API Designer</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full col-span-1 flex justify-center" title={pathname}>
|
||||
{filename}
|
||||
</div>
|
||||
<div className="menu-icon pr-2 col-span-1 flex justify-end">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setCreateApiSpecModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Create API Spec
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleOpenApiSpec();
|
||||
}}
|
||||
>
|
||||
Open API Spec
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<section className="main flex flex-grow px-4 relative">
|
||||
<div className="w-full grid grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<FileEditor apiSpec={apiSpec} />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Suspense fallback="">
|
||||
<Swagger string={raw} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiSpecPanel;
|
||||
@@ -1,198 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
|
||||
.titlebar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
padding-left: 70px; /* Space for macOS window controls */
|
||||
transition: padding-left 0.15s ease;
|
||||
}
|
||||
|
||||
/* When in full screen, no traffic lights so reduce padding */
|
||||
&.fullscreen .titlebar-content {
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
/* Remove drag region from interactive elements */
|
||||
.workspace-name-container,
|
||||
.dropdown-item,
|
||||
.home-button,
|
||||
.dropdown,
|
||||
button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* Left section */
|
||||
.titlebar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: 10px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* When in full screen, no traffic lights so remove margin-left */
|
||||
&.fullscreen .titlebar-left {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
/* Workspace Name Dropdown Trigger */
|
||||
.workspace-name-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
flex-shrink: 0;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* Center section - Bruno branding */
|
||||
.titlebar-center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
pointer-events: none;
|
||||
|
||||
.bruno-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.text};
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Right section */
|
||||
.titlebar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Workspace Dropdown Styles */
|
||||
.workspace-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 10px !important;
|
||||
margin: 0 !important;
|
||||
|
||||
&.active {
|
||||
.check-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.pin-btn:not(.pinned) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.workspace-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: ${(props) => props.theme.workspace?.accent || props.theme.colors?.text?.yellow};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pin-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
|
||||
opacity: 0;
|
||||
|
||||
&.pinned {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.dropdown.hoverBg};
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Adjust for non-macOS platforms */
|
||||
body:not(.os-mac) & {
|
||||
.titlebar-content {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Leave room for Windows caption buttons when the overlay is enabled */
|
||||
body.os-windows & {
|
||||
.titlebar-content {
|
||||
padding-right: 120px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,252 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconCheck, IconChevronDown, IconFolder, IconHome, IconLayoutColumns, IconLayoutRows, IconPin, IconPinned, IconPlus } from '@tabler/icons';
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { savePreferences, showHomePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs';
|
||||
import { openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces';
|
||||
|
||||
import Bruno from 'components/Bruno';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
|
||||
import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';
|
||||
|
||||
import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { toTitleCase } from 'utils/common/index';
|
||||
|
||||
const AppTitleBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||
|
||||
// Listen for fullscreen changes
|
||||
useEffect(() => {
|
||||
const { ipcRenderer } = window;
|
||||
if (!ipcRenderer) return;
|
||||
|
||||
const removeEnterFullScreenListener = ipcRenderer.on('main:enter-full-screen', () => {
|
||||
setIsFullScreen(true);
|
||||
});
|
||||
|
||||
const removeLeaveFullScreenListener = ipcRenderer.on('main:leave-full-screen', () => {
|
||||
setIsFullScreen(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeEnterFullScreenListener();
|
||||
removeLeaveFullScreenListener();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Get workspace info
|
||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen);
|
||||
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
|
||||
|
||||
// Sort workspaces according to preferences
|
||||
const sortedWorkspaces = useMemo(() => {
|
||||
return sortWorkspaces(workspaces, preferences);
|
||||
}, [workspaces, preferences]);
|
||||
|
||||
const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);
|
||||
|
||||
const WorkspaceName = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="workspace-name-container" {...props}>
|
||||
<span className="workspace-name">{toTitleCase(activeWorkspace?.name) || 'Default Workspace'}</span>
|
||||
<IconChevronDown size={14} stroke={1.5} className="chevron-icon" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleHomeClick = () => {
|
||||
dispatch(showHomePage());
|
||||
};
|
||||
|
||||
const handleWorkspaceSwitch = (workspaceUid) => {
|
||||
dispatch(switchWorkspace(workspaceUid));
|
||||
toast.success(`Switched to ${workspaces.find((w) => w.uid === workspaceUid)?.name}`);
|
||||
};
|
||||
|
||||
const handleOpenWorkspace = async () => {
|
||||
try {
|
||||
await dispatch(openWorkspaceDialog());
|
||||
toast.success('Workspace opened successfully');
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'Failed to open workspace');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateWorkspace = () => {
|
||||
setCreateWorkspaceModalOpen(true);
|
||||
};
|
||||
|
||||
const handlePinWorkspace = useCallback((workspaceUid, e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const newPreferences = toggleWorkspacePin(workspaceUid, preferences);
|
||||
dispatch(savePreferences(newPreferences));
|
||||
}, [dispatch, preferences]);
|
||||
|
||||
const orientation = preferences?.layout?.responsePaneOrientation || 'horizontal';
|
||||
|
||||
const handleToggleSidebar = () => {
|
||||
dispatch(toggleSidebarCollapse());
|
||||
};
|
||||
|
||||
const handleToggleDevtools = () => {
|
||||
if (isConsoleOpen) {
|
||||
dispatch(closeConsole());
|
||||
} else {
|
||||
dispatch(openConsole());
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleVerticalLayout = () => {
|
||||
const newOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
|
||||
const updatedPreferences = {
|
||||
...preferences,
|
||||
layout: {
|
||||
...preferences?.layout || {},
|
||||
responsePaneOrientation: newOrientation
|
||||
}
|
||||
};
|
||||
dispatch(savePreferences(updatedPreferences));
|
||||
};
|
||||
|
||||
// Build workspace menu items
|
||||
const workspaceMenuItems = useMemo(() => {
|
||||
const items = sortedWorkspaces.map((workspace) => {
|
||||
const isActive = workspace.uid === activeWorkspaceUid;
|
||||
const isPinned = preferences?.workspaces?.pinnedWorkspaceUids?.includes(workspace.uid);
|
||||
|
||||
return {
|
||||
id: workspace.uid,
|
||||
label: toTitleCase(workspace.name),
|
||||
onClick: () => handleWorkspaceSwitch(workspace.uid),
|
||||
className: `workspace-item ${isActive ? 'active' : ''}`,
|
||||
rightSection: (
|
||||
<div className="workspace-actions">
|
||||
{workspace.type !== 'default' && (
|
||||
<ActionIcon
|
||||
className={`pin-btn ${isPinned ? 'pinned' : ''}`}
|
||||
onClick={(e) => handlePinWorkspace(workspace.uid, e)}
|
||||
label={isPinned ? 'Unpin workspace' : 'Pin workspace'}
|
||||
size="sm"
|
||||
>
|
||||
{isPinned ? (
|
||||
<IconPinned size={14} stroke={1.5} />
|
||||
) : (
|
||||
<IconPin size={14} stroke={1.5} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
)}
|
||||
{isActive && <IconCheck size={16} stroke={1.5} className="check-icon" />}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
// Add label and action items
|
||||
items.push(
|
||||
{ type: 'label', label: 'Workspaces' },
|
||||
{
|
||||
id: 'create-workspace',
|
||||
leftSection: IconPlus,
|
||||
label: 'Create workspace',
|
||||
onClick: handleCreateWorkspace
|
||||
},
|
||||
{
|
||||
id: 'open-workspace',
|
||||
leftSection: IconFolder,
|
||||
label: 'Open workspace',
|
||||
onClick: handleOpenWorkspace
|
||||
}
|
||||
);
|
||||
|
||||
return items;
|
||||
}, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`app-titlebar ${isFullScreen ? 'fullscreen' : ''}`}>
|
||||
{createWorkspaceModalOpen && (
|
||||
<CreateWorkspace onClose={() => setCreateWorkspaceModalOpen(false)} />
|
||||
)}
|
||||
|
||||
<div className="titlebar-content">
|
||||
{/* Left section: Home + Workspace */}
|
||||
<div className="titlebar-left">
|
||||
<ActionIcon
|
||||
onClick={handleHomeClick}
|
||||
label="Home"
|
||||
size="lg"
|
||||
className="home-button"
|
||||
>
|
||||
<IconHome size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
{/* Workspace Dropdown */}
|
||||
<MenuDropdown
|
||||
data-testid="workspace-menu"
|
||||
items={workspaceMenuItems}
|
||||
placement="bottom-start"
|
||||
selectedItemId={activeWorkspaceUid}
|
||||
>
|
||||
<WorkspaceName />
|
||||
</MenuDropdown>
|
||||
</div>
|
||||
|
||||
{/* Center section: Bruno logo + text */}
|
||||
<div className="titlebar-center">
|
||||
<Bruno width={18} />
|
||||
<span className="bruno-text">Bruno</span>
|
||||
</div>
|
||||
|
||||
{/* Right section: Action buttons */}
|
||||
<div className="titlebar-right">
|
||||
{/* Toggle sidebar */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleSidebar}
|
||||
label={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
|
||||
size="lg"
|
||||
data-testid="toggle-sidebar-button"
|
||||
>
|
||||
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
{/* Toggle devtools */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleDevtools}
|
||||
label={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}
|
||||
size="lg"
|
||||
data-testid="toggle-devtools-button"
|
||||
>
|
||||
<IconBottombarToggle collapsed={!isConsoleOpen} size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
{/* Toggle vertical layout */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleVerticalLayout}
|
||||
label={orientation === 'horizontal' ? 'Switch to vertical layout' : 'Switch to horizontal layout'}
|
||||
size="lg"
|
||||
data-testid="toggle-vertical-layout-button"
|
||||
>
|
||||
{orientation === 'horizontal' ? (
|
||||
<IconLayoutColumns size={16} stroke={1.5} />
|
||||
) : (
|
||||
<IconLayoutRows size={16} stroke={1.5} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppTitleBar;
|
||||
@@ -373,7 +373,7 @@ const ClientCertSettings = ({ collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6 flex flex-row gap-2 items-center">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" data-testid="add-client-cert">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Add
|
||||
</button>
|
||||
<div className="h-4 border-l border-gray-600"></div>
|
||||
|
||||
@@ -3,23 +3,15 @@ import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
import { IconBox, IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
|
||||
import { areItemsLoading, getItemsLoadStats } from 'utils/collections/index';
|
||||
import { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import ShareCollection from 'components/ShareCollection/index';
|
||||
import { updateEnvironmentSettingsModalVisibility, updateGlobalEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
const Info = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
|
||||
|
||||
const isCollectionLoading = areItemsLoading(collection);
|
||||
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
|
||||
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
|
||||
|
||||
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
|
||||
|
||||
const collectionEnvironmentCount = collection.environments?.length || 0;
|
||||
const globalEnvironmentCount = globalEnvironments?.length || 0;
|
||||
|
||||
const handleToggleShowShareCollectionModal = (value) => (e) => {
|
||||
toggleShowShareCollectionModal(value);
|
||||
};
|
||||
@@ -47,24 +39,9 @@ const Info = ({ collection }) => {
|
||||
<IconWorld className="w-5 h-5 text-green-500" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-medium text-sm">Environments</div>
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
onClick={() => {
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(true));
|
||||
}}
|
||||
>
|
||||
{collectionEnvironmentCount} collection environment{collectionEnvironmentCount !== 1 ? 's' : ''}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
onClick={() => dispatch(updateGlobalEnvironmentSettingsModalVisibility(true))}
|
||||
>
|
||||
{globalEnvironmentCount} global environment{globalEnvironmentCount !== 1 ? 's' : ''}
|
||||
</button>
|
||||
<div className="font-medium">Environments</div>
|
||||
<div className="mt-1 text-muted text-xs">
|
||||
{collection.environments?.length || 0} environment{collection.environments?.length !== 1 ? 's' : ''} configured
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -20,7 +20,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
@@ -4,23 +4,18 @@ import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import DeprecationWarning from 'components/DeprecationWarning';
|
||||
|
||||
const Vars = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<VarsTable collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
|
||||
@@ -32,32 +32,17 @@ const CollectionSettings = ({ collection }) => {
|
||||
const hasTests = root?.request?.tests;
|
||||
const hasDocs = root?.docs;
|
||||
|
||||
const headers = collection.draft?.root
|
||||
? get(collection, 'draft.root.request.headers', [])
|
||||
: get(collection, 'root.request.headers', []);
|
||||
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
|
||||
const activeHeadersCount = headers.filter((header) => header.enabled).length;
|
||||
|
||||
const requestVars = collection.draft?.root
|
||||
? get(collection, 'draft.root.request.vars.req', [])
|
||||
: get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = collection.draft?.root
|
||||
? get(collection, 'draft.root.request.vars.res', [])
|
||||
: get(collection, 'root.request.vars.res', []);
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
const authMode
|
||||
= (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {}))
|
||||
.mode || 'none';
|
||||
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length;
|
||||
const authMode = (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {})).mode || 'none';
|
||||
|
||||
const proxyConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.proxy', {})
|
||||
: get(collection, 'brunoConfig.proxy', {});
|
||||
const proxyConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.proxy', {}) : get(collection, 'brunoConfig.proxy', {});
|
||||
const proxyEnabled = proxyConfig.hostname ? true : false;
|
||||
const clientCertConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.clientCertificates.certs', [])
|
||||
: get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const protobufConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.protobuf', {})
|
||||
: get(collection, 'brunoConfig.protobuf', {});
|
||||
const clientCertConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.clientCertificates.certs', []) : get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const protobufConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.protobuf', {}) : get(collection, 'brunoConfig.protobuf', {});
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
@@ -83,7 +68,11 @@ const CollectionSettings = ({ collection }) => {
|
||||
return <ProxySettings collection={collection} />;
|
||||
}
|
||||
case 'clientCert': {
|
||||
return <ClientCertSettings collection={collection} />;
|
||||
return (
|
||||
<ClientCertSettings
|
||||
collection={collection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'protobuf': {
|
||||
return <Protobuf collection={collection} />;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,172 +0,0 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { newHttpRequest, newGrpcRequest, newWsRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { generateUniqueRequestName } from 'utils/collections';
|
||||
import { sanitizeName } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconApi, IconBrandGraphql, IconPlugConnected, IconCode, IconPlus } from '@tabler/icons';
|
||||
|
||||
const CreateUntitledRequest = ({ collectionUid, itemUid = null, onRequestCreated, placement = 'bottom' }) => {
|
||||
const dispatch = useDispatch();
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const collection = collections?.find((c) => c.uid === collectionUid);
|
||||
const dropdownTippyRef = useRef();
|
||||
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleCreateHttpRequest = async () => {
|
||||
dropdownTippyRef.current?.hide();
|
||||
const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
dispatch(
|
||||
newHttpRequest({
|
||||
requestName: uniqueName,
|
||||
filename: filename,
|
||||
requestType: 'http-request',
|
||||
requestUrl: '',
|
||||
requestMethod: 'GET',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: itemUid
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('New request created!');
|
||||
onRequestCreated?.();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
};
|
||||
|
||||
const handleCreateGraphQLRequest = async () => {
|
||||
dropdownTippyRef.current?.hide();
|
||||
const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
dispatch(
|
||||
newHttpRequest({
|
||||
requestName: uniqueName,
|
||||
filename: filename,
|
||||
requestType: 'graphql-request',
|
||||
requestUrl: '',
|
||||
requestMethod: 'POST',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: itemUid,
|
||||
body: {
|
||||
mode: 'graphql',
|
||||
graphql: {
|
||||
query: '',
|
||||
variables: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('New request created!');
|
||||
onRequestCreated?.();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
};
|
||||
|
||||
const handleCreateWebSocketRequest = async () => {
|
||||
dropdownTippyRef.current?.hide();
|
||||
const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
dispatch(
|
||||
newWsRequest({
|
||||
requestName: uniqueName,
|
||||
filename: filename,
|
||||
requestUrl: '',
|
||||
requestMethod: 'ws',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: itemUid
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('New request created!');
|
||||
onRequestCreated?.();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
};
|
||||
|
||||
const handleCreateGrpcRequest = async () => {
|
||||
dropdownTippyRef.current?.hide();
|
||||
const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
dispatch(
|
||||
newGrpcRequest({
|
||||
requestName: uniqueName,
|
||||
filename: filename,
|
||||
requestUrl: '',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: itemUid
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('New request created!');
|
||||
onRequestCreated?.();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<IconPlus size={16} strokeWidth={2} />} placement={placement}>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleCreateHttpRequest();
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconApi size={16} strokeWidth={2} />
|
||||
</span>
|
||||
HTTP
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleCreateGraphQLRequest();
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconBrandGraphql size={16} strokeWidth={2} />
|
||||
</span>
|
||||
GraphQL
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleCreateWebSocketRequest();
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconPlugConnected size={16} strokeWidth={2} />
|
||||
</span>
|
||||
WebSocket
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleCreateGrpcRequest();
|
||||
}}
|
||||
>
|
||||
<span className="dropdown-icon">
|
||||
<IconCode size={16} strokeWidth={2} />
|
||||
</span>
|
||||
gRPC
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUntitledRequest;
|
||||
@@ -168,7 +168,7 @@ const StyledWrapper = styled.div`
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
|
||||
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
@@ -256,8 +256,10 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.response-body-container {
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -265,11 +267,13 @@ const StyledWrapper = styled.div`
|
||||
.w-full.h-full.relative.flex {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
background: ${(props) => props.theme.console.headerBg} !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
div[role="tablist"] {
|
||||
background: ${(props) => props.theme.console.dropdownHeaderBg};
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
display: flex !important;
|
||||
@@ -278,17 +282,28 @@ const StyledWrapper = styled.div`
|
||||
align-items: center !important;
|
||||
min-height: 40px !important;
|
||||
flex-shrink: 0 !important;
|
||||
|
||||
|
||||
> div {
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
font-size: ${(props) => props.theme.font.size.sm} !important;
|
||||
padding: 6px 12px !important;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
white-space: nowrap !important;
|
||||
min-width: auto !important;
|
||||
height: auto !important;
|
||||
line-height: 1.2 !important;
|
||||
font-weight: 500 !important;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
border-color: ${(props) => props.theme.console.buttonHoverBg};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.console.checkboxColor};
|
||||
color: white;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
IconNetwork
|
||||
} from '@tabler/icons';
|
||||
import { clearSelectedRequest } from 'providers/ReduxStore/slices/logs';
|
||||
import QueryResponse from 'components/ResponsePane/QueryResponse/index';
|
||||
import QueryResult from 'components/ResponsePane/QueryResult';
|
||||
import Network from 'components/ResponsePane/Timeline/TimelineItem/Network';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { uuid } from 'utils/common/index';
|
||||
@@ -116,7 +116,7 @@ const ResponseTab = ({ response, request, collection }) => {
|
||||
<h4>Response Body</h4>
|
||||
<div className="response-body-container">
|
||||
{response?.data || response?.dataBuffer ? (
|
||||
<QueryResponse
|
||||
<QueryResult
|
||||
item={{ uid: uuid() }}
|
||||
collection={collection}
|
||||
data={response.data}
|
||||
|
||||
@@ -25,16 +25,6 @@ const Wrapper = styled.div`
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
[role="menu"] {
|
||||
outline: none;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.label-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -69,10 +59,6 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
@@ -84,31 +70,10 @@ const Wrapper = styled.div`
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.dropdown-right-section {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&.selected-focused:not(:disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:focus-visible:not(:disabled) {
|
||||
outline: none;
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
|
||||
@@ -2,55 +2,46 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-environment {
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
padding: 0.25rem 0.3rem 0.25rem 0.5rem;
|
||||
border-radius: 0.9375rem;
|
||||
padding: 0.25rem 0.5rem 0.25rem 0.75rem;
|
||||
user-select: none;
|
||||
background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.bg};
|
||||
border: 1px solid ${(props) => props.theme.app.collection.toolbar.environmentSelector.border};
|
||||
background-color: transparent;
|
||||
border: 1px solid ${(props) => props.theme.dropdown.selectedColor};
|
||||
line-height: 1rem;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBorder};
|
||||
background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBg};
|
||||
}
|
||||
|
||||
.caret {
|
||||
margin-left: 0.25rem;
|
||||
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret};
|
||||
fill: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret};
|
||||
align-self: center;
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140, 140, 140);
|
||||
}
|
||||
|
||||
.env-icon {
|
||||
margin-right: 0.25rem;
|
||||
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.icon};
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
}
|
||||
|
||||
.env-text {
|
||||
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.text};
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
display: block;
|
||||
}
|
||||
|
||||
.env-separator {
|
||||
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.separator};
|
||||
margin: 0 0.35rem;
|
||||
color: #8c8c8c;
|
||||
margin: 0 0.25rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.env-text-inactive {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.no-environments {
|
||||
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.text};
|
||||
background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.bg};
|
||||
border: 1px dashed ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.border};
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.hoverBorder};
|
||||
background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.hoverBg};
|
||||
}
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border: 1px solid transparent;
|
||||
color: ${(props) => props.theme.dropdown.secondaryText};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import find from 'lodash/find';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { IconWorld, IconDatabase, IconCaretDown, IconSettings, IconPlus, IconDownload } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateEnvironmentSettingsModalVisibility, updateGlobalEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
|
||||
import { updateEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { selectGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -20,6 +20,8 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const [activeTab, setActiveTab] = useState('collection');
|
||||
const [showGlobalSettings, setShowGlobalSettings] = useState(false);
|
||||
const [showCollectionSettings, setShowCollectionSettings] = useState(false);
|
||||
const [showCreateGlobalModal, setShowCreateGlobalModal] = useState(false);
|
||||
const [showImportGlobalModal, setShowImportGlobalModal] = useState(false);
|
||||
const [showCreateCollectionModal, setShowCreateCollectionModal] = useState(false);
|
||||
@@ -27,8 +29,6 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
|
||||
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
|
||||
const activeGlobalEnvironmentUid = useSelector((state) => state.globalEnvironments.activeGlobalEnvironmentUid);
|
||||
const isEnvironmentSettingsModalOpen = useSelector((state) => state.app.isEnvironmentSettingsModalOpen);
|
||||
const isGlobalEnvironmentSettingsModalOpen = useSelector((state) => state.app.isGlobalEnvironmentSettingsModalOpen);
|
||||
const activeGlobalEnvironment = activeGlobalEnvironmentUid
|
||||
? find(globalEnvironments, (e) => e.uid === activeGlobalEnvironmentUid)
|
||||
: null;
|
||||
@@ -79,8 +79,9 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
const handleSettingsClick = () => {
|
||||
if (activeTab === 'collection') {
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(true));
|
||||
setShowCollectionSettings(true);
|
||||
} else {
|
||||
dispatch(updateGlobalEnvironmentSettingsModalVisibility(true));
|
||||
setShowGlobalSettings(true);
|
||||
}
|
||||
dropdownTippyRef.current.hide();
|
||||
};
|
||||
@@ -107,8 +108,9 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
|
||||
// Modal handlers
|
||||
const handleCloseSettings = () => {
|
||||
setShowGlobalSettings(false);
|
||||
setShowCollectionSettings(false);
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(false));
|
||||
dispatch(updateGlobalEnvironmentSettingsModalVisibility(false));
|
||||
};
|
||||
|
||||
// Calculate dropdown width based on the longest environment name.
|
||||
@@ -162,7 +164,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="env-text-inactive max-w-36 truncate no-wrap">No Environment</span>
|
||||
<span className="env-text-inactive max-w-36 truncate no-wrap">No environments</span>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -174,7 +176,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
data-testid="environment-selector-trigger"
|
||||
>
|
||||
{displayContent}
|
||||
<IconCaretDown className="caret flex items-center justify-center" size={12} strokeWidth={2} />
|
||||
<IconCaretDown className="caret" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -218,7 +220,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
{/* Modals - Rendered outside dropdown to avoid conflicts */}
|
||||
{isGlobalEnvironmentSettingsModalOpen && (
|
||||
{showGlobalSettings && (
|
||||
<GlobalEnvironmentSettings
|
||||
globalEnvironments={globalEnvironments}
|
||||
collection={collection}
|
||||
@@ -227,15 +229,13 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{isEnvironmentSettingsModalOpen && (
|
||||
<EnvironmentSettings collection={collection} onClose={handleCloseSettings} />
|
||||
)}
|
||||
{showCollectionSettings && <EnvironmentSettings collection={collection} onClose={handleCloseSettings} />}
|
||||
|
||||
{showCreateGlobalModal && (
|
||||
<CreateGlobalEnvironment
|
||||
onClose={() => setShowCreateGlobalModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
dispatch(updateGlobalEnvironmentSettingsModalVisibility(true));
|
||||
setShowGlobalSettings(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -245,7 +245,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
type="global"
|
||||
onClose={() => setShowImportGlobalModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
dispatch(updateGlobalEnvironmentSettingsModalVisibility(true));
|
||||
setShowGlobalSettings(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -255,7 +255,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
collection={collection}
|
||||
onClose={() => setShowCreateCollectionModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(true));
|
||||
setShowCollectionSettings(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -266,7 +266,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
collection={collection}
|
||||
onClose={() => setShowImportCollectionModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(true));
|
||||
setShowCollectionSettings(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -22,7 +22,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
@@ -8,19 +8,13 @@ import { useDispatch } from 'react-redux';
|
||||
const Vars = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = folder.draft ? get(folder, 'draft.request.vars.req', []) : get(folder, 'root.request.vars.req', []);
|
||||
const responseVars = folder.draft ? get(folder, 'draft.request.vars.res', []) : get(folder, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<VarsTable folder={folder} collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable folder={folder} collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
|
||||
@@ -28,8 +28,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
const activeHeadersCount = headers.filter((header) => header.enabled).length;
|
||||
|
||||
const requestVars = folderRoot?.request?.vars?.req || [];
|
||||
const responseVars = folderRoot?.request?.vars?.res || [];
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length;
|
||||
|
||||
const auth = get(folderRoot, 'request.auth.mode');
|
||||
const hasAuth = auth && auth !== 'none';
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const IconBottombarToggle = ({ collapsed = false, size = 16, strokeWidth = 1.5, className = '', ...rest }) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" className={`icon icon-tabler icons-tabler-outline icon-tabler-layout-bottombar ${className}`} {...rest}>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z" />
|
||||
<path d="M4 15l16 0" />
|
||||
{!collapsed && (
|
||||
<rect x="4.6" y="15.6" width="14.8" height="2.8" rx="0.8" fill="currentColor" />
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconBottombarToggle;
|
||||
@@ -88,9 +88,7 @@ const Modal = ({
|
||||
return closeModal({ type: 'esc' });
|
||||
}
|
||||
case ENTER_KEY_CODE: {
|
||||
// Skip if a submit button is focused - let native button click handle it to avoid double-fire
|
||||
const isSubmitButton = event.target?.type === 'submit';
|
||||
if (!shiftKey && !ctrlKey && !altKey && !metaKey && handleConfirm && !isSubmitButton) {
|
||||
if (!shiftKey && !ctrlKey && !altKey && !metaKey && handleConfirm) {
|
||||
return handleConfirm();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import get from 'lodash/get';
|
||||
import classnames from 'classnames';
|
||||
@@ -15,86 +15,54 @@ import Tests from 'components/RequestPane/Tests';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import Settings from 'components/RequestPane/Settings';
|
||||
import RequestPaneTabs from 'components/RequestPane/RequestPaneTabs';
|
||||
|
||||
const MULTIPLE_CONTENT_TABS = new Set(['script', 'vars', 'auth', 'docs']);
|
||||
|
||||
const TAB_CONFIG = [
|
||||
{ key: 'query', label: 'Query' },
|
||||
{ key: 'variables', label: 'Variables' },
|
||||
{ key: 'headers', label: 'Headers' },
|
||||
{ key: 'auth', label: 'Auth' },
|
||||
{ key: 'vars', label: 'Vars' },
|
||||
{ key: 'script', label: 'Script' },
|
||||
{ key: 'assert', label: 'Assert' },
|
||||
{ key: 'tests', label: 'Tests' },
|
||||
{ key: 'docs', label: 'Docs' },
|
||||
{ key: 'settings', label: 'Settings' }
|
||||
];
|
||||
|
||||
const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const query = item.draft
|
||||
? get(item, 'draft.request.body.graphql.query', '')
|
||||
: get(item, 'request.body.graphql.query', '');
|
||||
const variables = item.draft
|
||||
? get(item, 'draft.request.body.graphql.variables')
|
||||
: get(item, 'request.body.graphql.variables');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const [schema, setSchema] = useState(null);
|
||||
const schemaActionsRef = useRef(null);
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const requestPaneTab = focusedTab?.requestPaneTab;
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
useEffect(() => {
|
||||
onSchemaLoad(schema);
|
||||
}, [schema, onSchemaLoad]);
|
||||
}, [schema]);
|
||||
|
||||
const onQueryChange = useCallback(
|
||||
(value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlQuery({
|
||||
query: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, item.uid, collection.uid]
|
||||
);
|
||||
const onQueryChange = (value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlQuery({
|
||||
query: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const onRun = useCallback(
|
||||
() => dispatch(sendRequest(item, collection.uid)),
|
||||
[dispatch, item, collection.uid]
|
||||
);
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
updateRequestPaneTab({
|
||||
uid: item.uid,
|
||||
requestPaneTab: tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = useCallback(
|
||||
() => dispatch(saveRequest(item.uid, collection.uid)),
|
||||
[dispatch, item.uid, collection.uid]
|
||||
);
|
||||
|
||||
const selectTab = useCallback(
|
||||
(tabKey) => {
|
||||
dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: tabKey }));
|
||||
},
|
||||
[dispatch, item.uid]
|
||||
);
|
||||
|
||||
const allTabs = useMemo(() => TAB_CONFIG.map(({ key, label }) => ({ key, label })), []);
|
||||
|
||||
const tabPanel = useMemo(() => {
|
||||
switch (requestPaneTab) {
|
||||
case 'query':
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'query': {
|
||||
return (
|
||||
<QueryEditor
|
||||
collection={collection}
|
||||
@@ -109,55 +77,94 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
);
|
||||
case 'variables':
|
||||
}
|
||||
case 'variables': {
|
||||
return <GraphQLVariables item={item} variables={variables} collection={collection} />;
|
||||
case 'headers':
|
||||
}
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
case 'auth':
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth item={item} collection={collection} />;
|
||||
case 'vars':
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
case 'assert':
|
||||
}
|
||||
case 'assert': {
|
||||
return <Assertions item={item} collection={collection} />;
|
||||
case 'script':
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
case 'tests':
|
||||
}
|
||||
case 'tests': {
|
||||
return <Tests item={item} collection={collection} />;
|
||||
case 'docs':
|
||||
}
|
||||
case 'docs': {
|
||||
return <Documentation item={item} collection={collection} />;
|
||||
case 'settings':
|
||||
}
|
||||
case 'settings': {
|
||||
return <Settings item={item} collection={collection} />;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
}, [requestPaneTab, item, collection, displayedTheme, schema, onSave, query, onRun, onQueryChange, handleGqlClickReference, preferences, variables]);
|
||||
};
|
||||
|
||||
if (!activeTabUid || !focusedTab?.uid || !requestPaneTab) {
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const isMultipleContentTab = MULTIPLE_CONTENT_TABS.has(requestPaneTab);
|
||||
|
||||
const rightContent = (
|
||||
<div ref={schemaActionsRef}>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
);
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === focusedTab.requestPaneTab
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full relative">
|
||||
<RequestPaneTabs
|
||||
tabs={allTabs}
|
||||
activeTab={requestPaneTab}
|
||||
onTabSelect={selectTab}
|
||||
rightContent={rightContent}
|
||||
rightContentRef={schemaActionsRef}
|
||||
/>
|
||||
|
||||
<section className={classnames('flex w-full flex-1', { 'mt-5': !isMultipleContentTab })}>
|
||||
<HeightBoundContainer>{tabPanel}</HeightBoundContainer>
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('query')} role="tab" onClick={() => selectTab('query')}>
|
||||
Query
|
||||
</div>
|
||||
<div className={getTabClassname('variables')} role="tab" onClick={() => selectTab('variables')}>
|
||||
Variables
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||
Settings
|
||||
</div>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
<section className="flex w-full mt-5 flex-1 relative">
|
||||
<HeightBoundContainer>{getTabPanel(focusedTab.requestPaneTab)}</HeightBoundContainer>
|
||||
</section>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: 2.1rem;
|
||||
height: 2.3rem;
|
||||
border: ${(props) => props.theme.requestTabPanel.url.border};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -20,7 +20,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
&.tabs {
|
||||
div.more-tabs {
|
||||
color: var(--color-tab-inactive) !important;
|
||||
border-bottom: solid 2px transparent;
|
||||
}
|
||||
|
||||
div.tabs {
|
||||
div.tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
@@ -31,21 +20,12 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
.content-indicator {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
sup {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
margin-left: 0;
|
||||
color: ${(props) => props.theme.text}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { find, get } from 'lodash';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryParams from 'components/RequestPane/QueryParams';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
@@ -12,144 +11,175 @@ import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import Settings from 'components/RequestPane/Settings';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { find, get } from 'lodash';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import RequestPaneTabs from 'components/RequestPane/RequestPaneTabs';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
|
||||
const MULTIPLE_CONTENT_TABS = new Set(['params', 'script', 'vars', 'auth', 'docs']);
|
||||
|
||||
const TAB_CONFIG = [
|
||||
{ key: 'params', label: 'Params' },
|
||||
{ key: 'body', label: 'Body' },
|
||||
{ key: 'headers', label: 'Headers' },
|
||||
{ key: 'auth', label: 'Auth' },
|
||||
{ key: 'vars', label: 'Vars' },
|
||||
{ key: 'script', label: 'Script' },
|
||||
{ key: 'assert', label: 'Assert' },
|
||||
{ key: 'tests', label: 'Tests' },
|
||||
{ key: 'docs', label: 'Docs' },
|
||||
{ key: 'settings', label: 'Settings' }
|
||||
];
|
||||
|
||||
const TAB_PANELS = {
|
||||
params: QueryParams,
|
||||
body: RequestBody,
|
||||
headers: RequestHeaders,
|
||||
auth: Auth,
|
||||
vars: Vars,
|
||||
assert: Assertions,
|
||||
script: Script,
|
||||
tests: Tests,
|
||||
docs: Documentation,
|
||||
settings: Settings
|
||||
};
|
||||
import { useEffect } from 'react';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import Settings from 'components/RequestPane/Settings';
|
||||
|
||||
const HttpRequestPane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
|
||||
const bodyModeRef = useRef(null);
|
||||
const initialAutoSelectDone = useRef(false);
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
updateRequestPaneTab({
|
||||
uid: item.uid,
|
||||
requestPaneTab: tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'params': {
|
||||
return <QueryParams item={item} collection={collection} />;
|
||||
}
|
||||
case 'body': {
|
||||
return <RequestBody item={item} collection={collection} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
case 'assert': {
|
||||
return <Assertions item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <Tests item={item} collection={collection} />;
|
||||
}
|
||||
case 'docs': {
|
||||
return <Documentation item={item} collection={collection} />;
|
||||
}
|
||||
case 'settings': {
|
||||
return <Settings item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const requestPaneTab = focusedTab?.requestPaneTab;
|
||||
|
||||
const getProperty = useCallback(
|
||||
(key) => (item.draft ? get(item, `draft.${key}`, []) : get(item, key, [])),
|
||||
[item.draft, item]
|
||||
);
|
||||
|
||||
const params = getProperty('request.params');
|
||||
const body = getProperty('request.body');
|
||||
const headers = getProperty('request.headers');
|
||||
const script = getProperty('request.script');
|
||||
const assertions = getProperty('request.assertions');
|
||||
const tests = getProperty('request.tests');
|
||||
const docs = getProperty('request.docs');
|
||||
const requestVars = getProperty('request.vars.req');
|
||||
const responseVars = getProperty('request.vars.res');
|
||||
const auth = getProperty('request.auth');
|
||||
const tags = getProperty('tags');
|
||||
|
||||
const activeCounts = useMemo(() => ({
|
||||
params: params.filter((p) => p.enabled).length,
|
||||
headers: headers.filter((h) => h.enabled).length,
|
||||
assertions: assertions.filter((a) => a.enabled).length,
|
||||
vars: requestVars.filter((r) => r.enabled).length + responseVars.filter((r) => r.enabled).length
|
||||
}), [params, headers, assertions, requestVars, responseVars]);
|
||||
|
||||
const selectTab = useCallback(
|
||||
(tabKey) => {
|
||||
dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: tabKey }));
|
||||
},
|
||||
[dispatch, item.uid]
|
||||
);
|
||||
|
||||
const indicators = useMemo(() => {
|
||||
const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;
|
||||
const hasTestError = item.testScriptErrorMessage;
|
||||
|
||||
return {
|
||||
params: activeCounts.params > 0 ? <sup className="font-medium">{activeCounts.params}</sup> : null,
|
||||
body: body.mode !== 'none' ? <StatusDot /> : null,
|
||||
headers: activeCounts.headers > 0 ? <sup className="font-medium">{activeCounts.headers}</sup> : null,
|
||||
auth: auth.mode !== 'none' ? <StatusDot /> : null,
|
||||
vars: activeCounts.vars > 0 ? <sup className="font-medium">{activeCounts.vars}</sup> : null,
|
||||
script: (script.req || script.res) ? (hasScriptError ? <StatusDot type="error" /> : <StatusDot />) : null,
|
||||
assert: activeCounts.assertions > 0 ? <sup className="font-medium">{activeCounts.assertions}</sup> : null,
|
||||
tests: tests?.length > 0 ? (hasTestError ? <StatusDot type="error" /> : <StatusDot />) : null,
|
||||
docs: docs?.length > 0 ? <StatusDot /> : null,
|
||||
settings: tags?.length > 0 ? <StatusDot /> : null
|
||||
};
|
||||
}, [activeCounts, body.mode, auth.mode, script, item.preRequestScriptErrorMessage, item.postResponseScriptErrorMessage, item.testScriptErrorMessage, tests, docs, tags]);
|
||||
|
||||
const allTabs = useMemo(
|
||||
() => TAB_CONFIG.map(({ key, label }) => ({ key, label, indicator: indicators[key] })),
|
||||
[indicators]
|
||||
);
|
||||
|
||||
const tabPanel = useMemo(() => {
|
||||
const Component = TAB_PANELS[requestPaneTab];
|
||||
return Component ? <Component item={item} collection={collection} /> : <div className="mt-4">404 | Not found</div>;
|
||||
}, [requestPaneTab, item, collection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialAutoSelectDone.current && activeCounts.params === 0 && body.mode !== 'none') {
|
||||
selectTab('body');
|
||||
}
|
||||
initialAutoSelectDone.current = true;
|
||||
}, [activeCounts.params, body.mode, selectTab]);
|
||||
|
||||
if (!activeTabUid || !focusedTab?.uid || !requestPaneTab) {
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const isMultipleContentTab = MULTIPLE_CONTENT_TABS.has(requestPaneTab);
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === focusedTab.requestPaneTab
|
||||
});
|
||||
};
|
||||
|
||||
const rightContent = requestPaneTab === 'body' ? (
|
||||
<div ref={bodyModeRef}>
|
||||
<RequestBodyMode item={item} collection={collection} />
|
||||
</div>
|
||||
) : null;
|
||||
const isMultipleContentTab = ['params', 'script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab);
|
||||
|
||||
// get the length of active params, headers, asserts and vars as well as the contents of the body, tests and script
|
||||
const getPropertyFromDraftOrRequest = (propertyKey) =>
|
||||
item.draft ? get(item, `draft.${propertyKey}`, []) : get(item, propertyKey, []);
|
||||
const params = getPropertyFromDraftOrRequest('request.params');
|
||||
const body = getPropertyFromDraftOrRequest('request.body');
|
||||
const headers = getPropertyFromDraftOrRequest('request.headers');
|
||||
const script = getPropertyFromDraftOrRequest('request.script');
|
||||
const assertions = getPropertyFromDraftOrRequest('request.assertions');
|
||||
const tests = getPropertyFromDraftOrRequest('request.tests');
|
||||
const docs = getPropertyFromDraftOrRequest('request.docs');
|
||||
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
|
||||
const auth = getPropertyFromDraftOrRequest('request.auth');
|
||||
const tags = getPropertyFromDraftOrRequest('tags');
|
||||
|
||||
const activeParamsLength = params.filter((param) => param.enabled).length;
|
||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||
const activeAssertionsLength = assertions.filter((assertion) => assertion.enabled).length;
|
||||
const activeVarsLength = requestVars.filter((request) => request.enabled).length;
|
||||
|
||||
useEffect(() => {
|
||||
if (activeParamsLength === 0 && body.mode !== 'none') {
|
||||
selectTab('body');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full relative">
|
||||
<RequestPaneTabs
|
||||
tabs={allTabs}
|
||||
activeTab={requestPaneTab}
|
||||
onTabSelect={selectTab}
|
||||
rightContent={rightContent}
|
||||
rightContentRef={bodyModeRef}
|
||||
delayedTabs={['body']}
|
||||
/>
|
||||
|
||||
<section className={classnames('flex w-full flex-1', { 'mt-3': !isMultipleContentTab })}>
|
||||
<HeightBoundContainer>{tabPanel}</HeightBoundContainer>
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
|
||||
Params
|
||||
{activeParamsLength > 0 && <sup className="ml-1 font-medium">{activeParamsLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
Body
|
||||
{body.mode !== 'none' && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
{activeHeadersLength > 0 && <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
{auth.mode !== 'none' && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
{activeVarsLength > 0 && <sup className="ml-1 font-medium">{activeVarsLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
{(script.req || script.res) && (
|
||||
item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage
|
||||
? <StatusDot type="error" />
|
||||
: <StatusDot />
|
||||
)}
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
{activeAssertionsLength > 0 && <sup className="ml-1 font-medium">{activeAssertionsLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
{tests && tests.length > 0 && (
|
||||
item.testScriptErrorMessage
|
||||
? <StatusDot type="error" />
|
||||
: <StatusDot />
|
||||
)}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
{docs && docs.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||
Settings
|
||||
{tags && tags.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<RequestBodyMode item={item} collection={collection} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={classnames('flex w-full flex-1', {
|
||||
'mt-5': !isMultipleContentTab
|
||||
})}
|
||||
>
|
||||
<HeightBoundContainer>
|
||||
{getTabPanel(focusedTab.requestPaneTab)}
|
||||
</HeightBoundContainer>
|
||||
</section>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: 2.1rem;
|
||||
height: 2.3rem;
|
||||
border: ${(props) => props.theme.requestTabPanel.url.border};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import {
|
||||
requestUrlChanged,
|
||||
updateRequestMethod,
|
||||
setRequestHeaders,
|
||||
updateRequestBodyMode,
|
||||
updateRequestBody,
|
||||
updateRequestGraphqlQuery,
|
||||
updateRequestGraphqlVariables,
|
||||
updateRequestAuthMode,
|
||||
updateAuth
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest, cancelRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { getRequestFromCurlCommand } from 'utils/curl';
|
||||
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
|
||||
import { cancelRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import HttpMethodSelector from './HttpMethodSelector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { IconDeviceFloppy, IconArrowRight, IconCode, IconSquareRoundedX } from '@tabler/icons';
|
||||
@@ -92,289 +81,12 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleGraphqlPaste = useCallback((event) => {
|
||||
if (item.type !== 'graphql-request') {
|
||||
return;
|
||||
}
|
||||
|
||||
const clipboardData = event.clipboardData || window.clipboardData;
|
||||
const pastedData = clipboardData.getData('Text');
|
||||
|
||||
const curlCommandRegex = /^\s*curl\s/i;
|
||||
if (!curlCommandRegex.test(pastedData)) {
|
||||
toast.error('Invalid cURL command');
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
try {
|
||||
const request = getRequestFromCurlCommand(pastedData, 'graphql-request');
|
||||
if (!request || !request.url) {
|
||||
toast.error('Invalid cURL command');
|
||||
return;
|
||||
}
|
||||
// Update URL
|
||||
dispatch(requestUrlChanged({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
url: request.url
|
||||
}));
|
||||
|
||||
// Update method
|
||||
dispatch(updateRequestMethod({
|
||||
method: request.method.toUpperCase(), // Convert to uppercase
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
|
||||
// Update headers
|
||||
if (request.headers && request.headers.length > 0) {
|
||||
dispatch(setRequestHeaders({
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
headers: request.headers
|
||||
}));
|
||||
}
|
||||
|
||||
// Update body
|
||||
if (request.body) {
|
||||
const bodyMode = request.body.mode;
|
||||
if (bodyMode === 'graphql') {
|
||||
dispatch(updateRequestGraphqlQuery({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
query: request.body.graphql.query
|
||||
}));
|
||||
let variables = request.body.graphql.variables;
|
||||
try {
|
||||
variables = JSON.parse(variables);
|
||||
} catch (error) {
|
||||
// Keep variables as-is if JSON parsing fails
|
||||
}
|
||||
dispatch(updateRequestGraphqlVariables({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
variables: variables
|
||||
}));
|
||||
}
|
||||
|
||||
toast.success('GraphQL query imported successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing cURL command:', error);
|
||||
toast.error('Failed to parse GraphQL query');
|
||||
}
|
||||
}, [dispatch, item.uid, collection.uid]);
|
||||
|
||||
const handleHttpPaste = useCallback((event) => {
|
||||
// Only enable curl paste detection for HTTP requests
|
||||
if (item.type !== 'http-request') {
|
||||
return;
|
||||
}
|
||||
|
||||
const clipboardData = event.clipboardData || window.clipboardData;
|
||||
const pastedData = clipboardData.getData('Text');
|
||||
|
||||
// Check if pasted data looks like a cURL command
|
||||
const curlCommandRegex = /^\s*curl\s/i;
|
||||
if (!curlCommandRegex.test(pastedData)) {
|
||||
// Not a curl command, allow normal paste behavior
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent the default paste behavior
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
// Parse the curl command
|
||||
const request = getRequestFromCurlCommand(pastedData);
|
||||
if (!request || !request.url) {
|
||||
toast.error('Invalid cURL command');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update URL
|
||||
dispatch(
|
||||
requestUrlChanged({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
url: request.url
|
||||
})
|
||||
);
|
||||
|
||||
// Update method
|
||||
if (request.method) {
|
||||
dispatch(
|
||||
updateRequestMethod({
|
||||
method: request.method.toUpperCase(), // Convert to uppercase
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Update headers
|
||||
if (request.headers && request.headers.length > 0) {
|
||||
dispatch(
|
||||
setRequestHeaders({
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
headers: request.headers
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Update body
|
||||
if (request.body) {
|
||||
const bodyMode = request.body.mode;
|
||||
|
||||
// Set body mode first
|
||||
dispatch(
|
||||
updateRequestBodyMode({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
mode: bodyMode
|
||||
})
|
||||
);
|
||||
|
||||
// Set body content based on mode
|
||||
if (bodyMode === 'json' && request.body.json) {
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
content: request.body.json
|
||||
})
|
||||
);
|
||||
} else if (bodyMode === 'text' && request.body.text) {
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
content: request.body.text
|
||||
})
|
||||
);
|
||||
} else if (bodyMode === 'xml' && request.body.xml) {
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
content: request.body.xml
|
||||
})
|
||||
);
|
||||
} else if (bodyMode === 'graphql' && request.body.graphql) {
|
||||
if (request.body.graphql.query) {
|
||||
dispatch(
|
||||
updateRequestGraphqlQuery({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
query: request.body.graphql.query
|
||||
})
|
||||
);
|
||||
}
|
||||
if (request.body.graphql.variables) {
|
||||
dispatch(
|
||||
updateRequestGraphqlVariables({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
variables: request.body.graphql.variables
|
||||
})
|
||||
);
|
||||
}
|
||||
} else if (bodyMode === 'formUrlEncoded' && request.body.formUrlEncoded) {
|
||||
// For formUrlEncoded, we need to set each param individually
|
||||
// This is a limitation - we'd need to clear existing params first
|
||||
// For now, we'll set the body mode and the user can manually adjust
|
||||
// TODO: Implement proper formUrlEncoded param setting
|
||||
} else if (bodyMode === 'multipartForm' && request.body.multipartForm) {
|
||||
// For multipartForm, similar limitation
|
||||
// TODO: Implement proper multipartForm param setting
|
||||
}
|
||||
}
|
||||
|
||||
// Update auth
|
||||
if (request.auth) {
|
||||
const authMode = request.auth.mode;
|
||||
if (authMode) {
|
||||
dispatch(
|
||||
updateRequestAuthMode({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
mode: authMode
|
||||
})
|
||||
);
|
||||
|
||||
// Set auth content based on mode
|
||||
if (request.auth.basic) {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: request.auth.basic
|
||||
})
|
||||
);
|
||||
} else if (request.auth.bearer) {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'bearer',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: request.auth.bearer
|
||||
})
|
||||
);
|
||||
} else if (request.auth.digest) {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'digest',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: request.auth.digest
|
||||
})
|
||||
);
|
||||
} else if (request.auth.ntlm) {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: request.auth.ntlm
|
||||
})
|
||||
);
|
||||
} else if (request.auth.awsv4) {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: request.auth.awsv4
|
||||
})
|
||||
);
|
||||
} else if (request.auth.apikey) {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'apikey',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: request.auth.apikey
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toast.success('cURL command imported successfully');
|
||||
} catch (error) {
|
||||
console.error('Error parsing cURL command:', error);
|
||||
toast.error('Failed to parse cURL command');
|
||||
}
|
||||
},
|
||||
[dispatch, item.uid, item.type, collection.uid]
|
||||
);
|
||||
const handleCancelRequest = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dispatch(cancelRequest(item.cancelTokenUid, item, collection));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex items-center">
|
||||
<div className="flex flex-1 items-center h-full method-selector-container">
|
||||
@@ -398,12 +110,10 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<SingleLineEditor
|
||||
ref={editorRef}
|
||||
value={url}
|
||||
placeholder="Enter URL or paste a cURL request"
|
||||
onSave={(finalValue) => onSave(finalValue)}
|
||||
theme={storedTheme}
|
||||
onChange={(newValue) => onUrlChange(newValue)}
|
||||
onRun={handleRun}
|
||||
onPaste={item.type === 'http-request' ? handleHttpPaste : item.type === 'graphql-request' ? handleGraphqlPaste : null}
|
||||
collection={collection}
|
||||
highlightPathParams={true}
|
||||
item={item}
|
||||
@@ -417,7 +127,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
handleGenerateCode(e);
|
||||
}}
|
||||
>
|
||||
<IconCode color={theme.requestTabs.icon.color} strokeWidth={1.5} size={20} className="cursor-pointer" />
|
||||
<IconCode color={theme.requestTabs.icon.color} strokeWidth={1.5} size={22} className="cursor-pointer" />
|
||||
<span className="infotiptext text-xs">Generate Code</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -432,7 +142,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<IconDeviceFloppy
|
||||
color={hasChanges ? theme.colors.text.yellow : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
size={22}
|
||||
className={`${hasChanges ? 'cursor-pointer' : 'cursor-default'}`}
|
||||
/>
|
||||
<span className="infotiptext text-xs">
|
||||
@@ -443,7 +153,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<IconSquareRoundedX
|
||||
color={theme.requestTabPanel.url.iconDanger}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
size={22}
|
||||
data-testid="cancel-request-icon"
|
||||
onClick={handleCancelRequest}
|
||||
/>
|
||||
@@ -451,7 +161,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<IconArrowRight
|
||||
color={theme.requestTabPanel.url.icon}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
size={22}
|
||||
data-testid="send-arrow-icon"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,6 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
min-width: 125px;
|
||||
|
||||
.body-mode-selector {
|
||||
background: transparent;
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { IconChevronDown } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DROPDOWN_WIDTH = 60;
|
||||
const CALCULATION_DELAY_DEFAULT = 20;
|
||||
const CALCULATION_DELAY_EXTENDED = 150;
|
||||
|
||||
const RequestPaneTabs = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabSelect,
|
||||
rightContent,
|
||||
rightContentRef,
|
||||
delayedTabs = []
|
||||
}) => {
|
||||
const [visibleTabs, setVisibleTabs] = useState([]);
|
||||
const [overflowTabs, setOverflowTabs] = useState([]);
|
||||
|
||||
const tabsContainerRef = useRef(null);
|
||||
const tabRefsMap = useRef({});
|
||||
const dropdownTippyRef = useRef(null);
|
||||
|
||||
const handleTabSelect = useCallback(
|
||||
(tabKey) => {
|
||||
onTabSelect(tabKey);
|
||||
dropdownTippyRef.current?.hide();
|
||||
},
|
||||
[onTabSelect]
|
||||
);
|
||||
|
||||
const calculateTabVisibility = useCallback(() => {
|
||||
const container = tabsContainerRef.current;
|
||||
if (!container || !tabs.length) return;
|
||||
|
||||
const containerWidth = container.offsetWidth;
|
||||
const rightContentWidth = rightContentRef?.current
|
||||
? rightContentRef.current.offsetWidth + 20
|
||||
: 0;
|
||||
|
||||
const availableWidth = containerWidth - rightContentWidth - DROPDOWN_WIDTH;
|
||||
const visible = [];
|
||||
const overflow = [];
|
||||
let currentWidth = 0;
|
||||
|
||||
for (const tab of tabs) {
|
||||
const tabElement = tabRefsMap.current[tab.key];
|
||||
const tabWidth = tabElement ? tabElement.offsetWidth + 20 : 100;
|
||||
|
||||
if (currentWidth + tabWidth <= availableWidth && !overflow.length) {
|
||||
visible.push(tab);
|
||||
currentWidth += tabWidth;
|
||||
} else {
|
||||
overflow.push(tab);
|
||||
}
|
||||
}
|
||||
|
||||
if (!visible.some((t) => t.key === activeTab) && overflow.length) {
|
||||
const activeTabIndex = overflow.findIndex((t) => t.key === activeTab);
|
||||
if (activeTabIndex !== -1) {
|
||||
const [activeTabItem] = overflow.splice(activeTabIndex, 1);
|
||||
const lastVisible = visible.pop();
|
||||
if (lastVisible) overflow.unshift(lastVisible);
|
||||
visible.push(activeTabItem);
|
||||
}
|
||||
}
|
||||
|
||||
setVisibleTabs(visible);
|
||||
setOverflowTabs(overflow);
|
||||
}, [tabs, activeTab, rightContentRef]);
|
||||
|
||||
const renderTab = useCallback(
|
||||
(tab, isInDropdown = false) => {
|
||||
const isActive = tab.key === activeTab;
|
||||
|
||||
if (isInDropdown) {
|
||||
return (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={classnames('dropdown-item', { active: isActive })}
|
||||
role="tab"
|
||||
onClick={() => handleTabSelect(tab.key)}
|
||||
>
|
||||
<span className="flex items-center gap-1">
|
||||
{tab.label}
|
||||
{tab.indicator}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={classnames('tab select-none', tab.key, { active: isActive })}
|
||||
role="tab"
|
||||
onClick={() => handleTabSelect(tab.key)}
|
||||
ref={(el) => el && (tabRefsMap.current[tab.key] = el)}
|
||||
>
|
||||
{tab.label}
|
||||
{tab.indicator}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[activeTab, handleTabSelect]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const delay = delayedTabs.includes(activeTab) ? CALCULATION_DELAY_EXTENDED : CALCULATION_DELAY_DEFAULT;
|
||||
const timeoutId = setTimeout(() => requestAnimationFrame(calculateTabVisibility), delay);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [calculateTabVisibility, activeTab, delayedTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
let frameId = null;
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (frameId) cancelAnimationFrame(frameId);
|
||||
frameId = requestAnimationFrame(calculateTabVisibility);
|
||||
});
|
||||
|
||||
if (tabsContainerRef.current) observer.observe(tabsContainerRef.current);
|
||||
if (rightContentRef?.current) observer.observe(rightContentRef.current);
|
||||
|
||||
return () => {
|
||||
if (frameId) cancelAnimationFrame(frameId);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [calculateTabVisibility, rightContentRef]);
|
||||
|
||||
const hiddenStyle = useMemo(
|
||||
() => ({ visibility: 'hidden', position: 'absolute', display: 'flex', pointerEvents: 'none' }),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper ref={tabsContainerRef} className="flex items-center tabs" role="tablist">
|
||||
<div style={hiddenStyle}>
|
||||
{tabs.map((tab) => (
|
||||
<div
|
||||
key={tab.key}
|
||||
className={classnames('tab select-none', tab.key, { active: tab.key === activeTab })}
|
||||
ref={(el) => el && (tabRefsMap.current[tab.key] = el)}
|
||||
>
|
||||
{tab.label}
|
||||
{tab.indicator}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{visibleTabs.map((tab) => renderTab(tab))}
|
||||
|
||||
{overflowTabs.length > 0 && (
|
||||
<Dropdown
|
||||
icon={(
|
||||
<div className="more-tabs select-none flex items-center cursor-pointer gap-1">
|
||||
<span>More</span>
|
||||
<IconChevronDown size={14} strokeWidth={2} />
|
||||
</div>
|
||||
)}
|
||||
placement="bottom-start"
|
||||
onCreate={(instance) => (dropdownTippyRef.current = instance)}
|
||||
>
|
||||
<div style={{ minWidth: '150px' }}>{overflowTabs.map((tab) => renderTab(tab, true))}</div>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
{rightContent && (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
{rightContent}
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestPaneTabs;
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import DeprecationWarning from 'components/DeprecationWarning';
|
||||
|
||||
const Vars = ({ item, collection }) => {
|
||||
const requestVars = item.draft ? get(item, 'draft.request.vars.req') : get(item, 'request.vars.req');
|
||||
@@ -10,13 +11,8 @@ const Vars = ({ item, collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<VarsTable item={item} collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable item={item} collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
height: 2.1rem;
|
||||
height: 2.3rem;
|
||||
position: relative;
|
||||
border: ${(props) => props.theme.requestTabPanel.url.border};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
|
||||
@@ -243,7 +243,7 @@ const RequestTabPanel = () => {
|
||||
isVerticalLayout ? 'vertical-layout' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="pt-3 pb-3 px-4">
|
||||
<div className="pt-4 pb-3 px-4">
|
||||
{
|
||||
isGrpcRequest
|
||||
? <GrpcQueryUrl item={item} collection={collection} handleRun={handleRun} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { uuid } from 'utils/common';
|
||||
import { IconBox, IconRun, IconEye, IconSettings } from '@tabler/icons';
|
||||
import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons';
|
||||
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -45,28 +45,26 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center py-2 px-4">
|
||||
<div className="flex flex-1 items-center cursor-pointer hover:underline" onClick={viewCollectionSettings}>
|
||||
<IconBox size={18} strokeWidth={1.5} />
|
||||
<IconFiles size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2 mr-4 font-medium">{collection?.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-3 items-center justify-end">
|
||||
<span className="mr-2">
|
||||
<JsSandboxMode collection={collection} />
|
||||
</span>
|
||||
<span className="mr-3">
|
||||
<ToolHint text="Runner" toolhintId="RunnnerToolhintId" place="bottom">
|
||||
<IconRun className="cursor-pointer" size={16} strokeWidth={1.5} onClick={handleRun} />
|
||||
<IconRun className="cursor-pointer" size={18} strokeWidth={1.5} onClick={handleRun} />
|
||||
</ToolHint>
|
||||
</span>
|
||||
<span className="mr-3">
|
||||
<ToolHint text="Variables" toolhintId="VariablesToolhintId">
|
||||
<IconEye className="cursor-pointer" size={16} strokeWidth={1.5} onClick={viewVariables} />
|
||||
<IconEye className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewVariables} />
|
||||
</ToolHint>
|
||||
</span>
|
||||
<span className="mr-3">
|
||||
<ToolHint text="Collection Settings" toolhintId="CollectionSettingsToolhintId">
|
||||
<IconSettings className="cursor-pointer" size={16} strokeWidth={1.5} onClick={viewCollectionSettings} />
|
||||
</ToolHint>
|
||||
</span>
|
||||
<span className="mr-2">
|
||||
<ToolHint text="Javascript Sandbox" toolhintId="JavascriptSandboxToolhintId" place="bottom">
|
||||
<JsSandboxMode collection={collection} />
|
||||
<IconSettings className="cursor-pointer" size={18} strokeWidth={1.5} onClick={viewCollectionSettings} />
|
||||
</ToolHint>
|
||||
</span>
|
||||
<span>
|
||||
|
||||
@@ -8,7 +8,8 @@ import ExampleIcon from 'components/Icons/ExampleIcon';
|
||||
import ConfirmRequestClose from '../RequestTab/ConfirmRequestClose';
|
||||
import RequestTabNotFound from '../RequestTab/RequestTabNotFound';
|
||||
import StyledWrapper from '../RequestTab/StyledWrapper';
|
||||
import GradientCloseButton from '../RequestTab/GradientCloseButton';
|
||||
import CloseTabIcon from '../RequestTab/CloseTabIcon';
|
||||
import DraftTabIcon from '../RequestTab/DraftTabIcon';
|
||||
|
||||
const ExampleTab = ({ tab, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -58,7 +59,7 @@ const ExampleTab = ({ tab, collection }) => {
|
||||
if (!item || !example) {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="flex items-center justify-between tab-container px-3"
|
||||
className="flex items-center justify-between tab-container px-1"
|
||||
onMouseUp={(e) => {
|
||||
if (e.button === 1) {
|
||||
e.preventDefault();
|
||||
@@ -74,7 +75,7 @@ const ExampleTab = ({ tab, collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-3">
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
{showConfirmClose && (
|
||||
<ConfirmRequestClose
|
||||
item={item}
|
||||
@@ -115,13 +116,13 @@ const ExampleTab = ({ tab, collection }) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ExampleIcon size={14} color="currentColor" className="mr-1.5 text-gray-500 flex-shrink-0" />
|
||||
<ExampleIcon size={16} color="currentColor" className="mr-2 text-gray-500 flex-shrink-0" />
|
||||
<span className="tab-name" title={example.name}>
|
||||
{example.name}
|
||||
</span>
|
||||
</div>
|
||||
<GradientCloseButton
|
||||
hasChanges={hasChanges}
|
||||
<div
|
||||
className="flex px-2 close-icon-container"
|
||||
onClick={(e) => {
|
||||
if (!hasChanges) {
|
||||
return handleCloseClick(e);
|
||||
@@ -131,7 +132,13 @@ const ExampleTab = ({ tab, collection }) => {
|
||||
e.preventDefault();
|
||||
setShowConfirmClose(true);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{!hasChanges ? (
|
||||
<CloseTabIcon />
|
||||
) : (
|
||||
<DraftTabIcon />
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div.attrs((props) => ({
|
||||
style: {
|
||||
'--gradient-color': props.theme.requestTabs.bg,
|
||||
'--gradient-color-active': props.theme.bg
|
||||
}
|
||||
}))`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
width: 44px;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
top: 0;
|
||||
padding-right: 4px;
|
||||
z-index: 3;
|
||||
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--gradient-color) 40%
|
||||
);
|
||||
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
li.active & {
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--gradient-color-active) 40%
|
||||
);
|
||||
}
|
||||
|
||||
li:hover &,
|
||||
&.has-changes {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.close-icon-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
cursor: pointer;
|
||||
transition: background-color 0.12s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.requestTabs.icon.hoverBg};
|
||||
|
||||
.close-icon {
|
||||
color: ${(props) => props.theme.requestTabs.icon.hoverColor};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
color: ${(props) => props.theme.requestTabs.icon.color};
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
transition: color 0.12s ease;
|
||||
}
|
||||
|
||||
.has-changes-icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.draft-icon-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.close-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.has-changes:not(li:hover &) {
|
||||
.draft-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.close-icon-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
li:hover &.has-changes {
|
||||
.draft-icon-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.close-icon-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import CloseTabIcon from '../CloseTabIcon';
|
||||
import DraftTabIcon from '../DraftTabIcon';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GradientCloseButton = ({ onClick, hasChanges = false }) => {
|
||||
return (
|
||||
<StyledWrapper className={`close-gradient ${hasChanges ? 'has-changes' : ''}`}>
|
||||
<div className="close-icon-container" onClick={onClick} data-testid="request-tab-close-icon">
|
||||
<span className="draft-icon-wrapper">
|
||||
<DraftTabIcon />
|
||||
</span>
|
||||
<span className="close-icon-wrapper">
|
||||
<CloseTabIcon />
|
||||
</span>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GradientCloseButton;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import GradientCloseButton from './GradientCloseButton';
|
||||
import CloseTabIcon from './CloseTabIcon';
|
||||
import DraftTabIcon from './DraftTabIcon';
|
||||
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
|
||||
|
||||
const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDraft }) => {
|
||||
@@ -7,49 +8,49 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDra
|
||||
switch (type) {
|
||||
case 'collection-settings': {
|
||||
return (
|
||||
<>
|
||||
<IconSettings size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">Collection</span>
|
||||
</>
|
||||
<div onDoubleClick={handleDoubleClick} className="flex items-center flex-nowrap overflow-hidden">
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1 leading-6">Collection</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case 'collection-overview': {
|
||||
return (
|
||||
<>
|
||||
<IconSettings size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">Overview</span>
|
||||
<IconSettings size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1 leading-6">Collection</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'security-settings': {
|
||||
return (
|
||||
<>
|
||||
<IconShieldLock size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">Security</span>
|
||||
<IconShieldLock size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Security</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'folder-settings': {
|
||||
return (
|
||||
<>
|
||||
<IconFolder size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">{tabName || 'Folder'}</span>
|
||||
</>
|
||||
<div onDoubleClick={handleDoubleClick} className="flex items-center flex-nowrap overflow-hidden">
|
||||
<IconFolder size={18} strokeWidth={1.5} className="text-yellow-600 min-w-[18px]" />
|
||||
<span className="ml-1 leading-6 truncate">{tabName || 'Folder'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case 'variables': {
|
||||
return (
|
||||
<>
|
||||
<IconVariable size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">Variables</span>
|
||||
<IconVariable size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1 leading-6">Variables</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'collection-runner': {
|
||||
return (
|
||||
<>
|
||||
<IconRun size={14} strokeWidth={1.5} className="text-yellow-600 flex-shrink-0" />
|
||||
<span className="ml-1 tab-name">Runner</span>
|
||||
<IconRun size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1 leading-6">Runner</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -58,13 +59,10 @@ const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDra
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex items-baseline tab-label"
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
{getTabInfo(type, tabName)}
|
||||
<div className="flex items-center tab-label pl-2">{getTabInfo(type, tabName)}</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
{hasDraft ? <DraftTabIcon /> : <CloseTabIcon />}
|
||||
</div>
|
||||
<GradientCloseButton hasChanges={hasDraft} onClick={(e) => handleCloseClick(e)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,32 +1,43 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.tab-label {
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tab-method {
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.02em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-name {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
// so that the name does not cutoff when italicized
|
||||
padding-right: 2px;
|
||||
.close-icon-container {
|
||||
min-height: 20px;
|
||||
min-width: 24px;
|
||||
margin-left: 4px;
|
||||
border-radius: 3px;
|
||||
|
||||
.close-icon {
|
||||
display: none;
|
||||
color: ${(props) => props.theme.requestTabs.icon.color};
|
||||
width: 8px;
|
||||
padding-bottom: 6px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:hover .close-icon {
|
||||
color: ${(props) => props.theme.requestTabs.icon.hoverColor};
|
||||
background-color: ${(props) => props.theme.requestTabs.icon.hoverBg};
|
||||
}
|
||||
|
||||
.has-changes-icon {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.tab-method {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState, useRef, Fragment, useMemo, useEffect } from 'react';
|
||||
import React, { useCallback, useState, useRef, Fragment, useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
|
||||
import { saveRequest, saveCollectionRoot, saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -17,17 +17,16 @@ import StyledWrapper from './StyledWrapper';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
|
||||
import NewRequest from 'components/Sidebar/NewRequest/index';
|
||||
import GradientCloseButton from './GradientCloseButton';
|
||||
import CloseTabIcon from './CloseTabIcon';
|
||||
import DraftTabIcon from './DraftTabIcon';
|
||||
import { flattenItems } from 'utils/collections/index';
|
||||
import { closeWsConnection } from 'utils/network/index';
|
||||
import ExampleTab from '../ExampleTab';
|
||||
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid, hasOverflow, setHasOverflow }) => {
|
||||
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
|
||||
const tabNameRef = useRef(null);
|
||||
const lastOverflowStateRef = useRef(null);
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
const [showConfirmCollectionClose, setShowConfirmCollectionClose] = useState(false);
|
||||
const [showConfirmFolderClose, setShowConfirmFolderClose] = useState(false);
|
||||
@@ -37,48 +36,6 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
|
||||
const item = findItemInCollection(collection, tab.uid);
|
||||
|
||||
const method = useMemo(() => {
|
||||
if (!item) return;
|
||||
switch (item.type) {
|
||||
case 'grpc-request':
|
||||
return 'gRPC';
|
||||
case 'ws-request':
|
||||
return 'WS';
|
||||
case 'graphql-request':
|
||||
return 'GQL';
|
||||
default:
|
||||
return item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
}
|
||||
}, [item]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!item || !tabNameRef.current || !setHasOverflow) return;
|
||||
|
||||
const checkOverflow = () => {
|
||||
if (tabNameRef.current && setHasOverflow) {
|
||||
const hasOverflow = tabNameRef.current.scrollWidth > tabNameRef.current.clientWidth;
|
||||
if (lastOverflowStateRef.current !== hasOverflow) {
|
||||
lastOverflowStateRef.current = hasOverflow;
|
||||
setHasOverflow(hasOverflow);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(checkOverflow, 0);
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
checkOverflow();
|
||||
});
|
||||
|
||||
if (tabNameRef.current) {
|
||||
resizeObserver.observe(tabNameRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [item, item?.name, method, setHasOverflow]);
|
||||
|
||||
const handleCloseClick = (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -148,12 +105,11 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
|
||||
const hasDraft = tab.type === 'collection-settings' && collection?.draft;
|
||||
const hasFolderDraft = tab.type === 'folder-settings' && folder?.draft;
|
||||
|
||||
if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
|
||||
return (
|
||||
<StyledWrapper
|
||||
className={`flex items-center justify-between tab-container px-2 ${tab.preview ? 'italic' : ''}`}
|
||||
onMouseUp={handleMouseUp}
|
||||
className={`flex items-center justify-between tab-container px-1 ${tab.preview ? 'italic' : ''}`}
|
||||
onMouseUp={handleMouseUp} // Add middle-click behavior here
|
||||
>
|
||||
{showConfirmCollectionClose && tab.type === 'collection-settings' && (
|
||||
<ConfirmCollectionClose
|
||||
@@ -236,6 +192,21 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
);
|
||||
}
|
||||
|
||||
const getMethodText = useCallback((item) => {
|
||||
if (!item) return;
|
||||
|
||||
switch (item.type) {
|
||||
case 'grpc-request':
|
||||
return 'gRPC';
|
||||
case 'ws-request':
|
||||
return 'WS';
|
||||
case 'graphql-request':
|
||||
return 'GQL';
|
||||
default:
|
||||
return item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
}
|
||||
}, [item]);
|
||||
|
||||
const hasChanges = useMemo(() => hasRequestChanges(item), [item]);
|
||||
|
||||
if (!item) {
|
||||
@@ -257,36 +228,10 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
}
|
||||
|
||||
const isWS = item.type === 'ws-request';
|
||||
|
||||
useEffect(() => {
|
||||
const checkOverflow = () => {
|
||||
if (tabNameRef.current && setHasOverflow) {
|
||||
const hasOverflow = tabNameRef.current.scrollWidth > tabNameRef.current.clientWidth;
|
||||
if (lastOverflowStateRef.current !== hasOverflow) {
|
||||
lastOverflowStateRef.current = hasOverflow;
|
||||
setHasOverflow(hasOverflow);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(checkOverflow, 0);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
checkOverflow();
|
||||
});
|
||||
|
||||
if (tabNameRef.current) {
|
||||
resizeObserver.observe(tabNameRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [item.name, method, setHasOverflow]);
|
||||
const method = getMethodText(item);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-2">
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
{showConfirmClose && (
|
||||
<ConfirmRequestClose
|
||||
item={item}
|
||||
@@ -323,7 +268,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`flex items-baseline tab-label ${tab.preview ? 'italic' : ''}`}
|
||||
className={`flex items-baseline tab-label pl-2 ${tab.preview ? 'italic' : ''}`}
|
||||
onContextMenu={handleRightClick}
|
||||
onDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))}
|
||||
onMouseUp={(e) => {
|
||||
@@ -339,7 +284,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
<span className="tab-method uppercase" style={{ color: getMethodColor(method) }}>
|
||||
{method}
|
||||
</span>
|
||||
<span ref={tabNameRef} className="ml-1 tab-name" title={item.name}>
|
||||
<span className="ml-1 tab-name" title={item.name}>
|
||||
{item.name}
|
||||
</span>
|
||||
<RequestTabMenu
|
||||
@@ -352,19 +297,25 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
</div>
|
||||
<GradientCloseButton
|
||||
hasChanges={hasChanges}
|
||||
<div
|
||||
className="flex px-2 close-icon-container"
|
||||
onClick={(e) => {
|
||||
if (!hasChanges) {
|
||||
isWS && closeWsConnection(item.uid);
|
||||
return handleCloseClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowConfirmClose(true);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{!hasChanges ? (
|
||||
<CloseTabIcon />
|
||||
) : (
|
||||
<DraftTabIcon />
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
@@ -398,7 +349,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
|
||||
}
|
||||
|
||||
dispatch(closeTabs({ tabUids: [tabUid] }));
|
||||
} catch (err) { }
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
function handleRevertChanges(event) {
|
||||
@@ -417,7 +368,7 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
}
|
||||
} catch (err) { }
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
function handleCloseOtherTabs(event) {
|
||||
|
||||
@@ -1,44 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.tabs-scroll-container {
|
||||
overflow-x: auto;
|
||||
overflow-y: clip;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: -10px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
scrollbar-width: none;
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
|
||||
ul {
|
||||
padding: 0 2px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: scroll;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
@@ -48,127 +17,57 @@ const Wrapper = styled.div`
|
||||
|
||||
li {
|
||||
display: inline-flex;
|
||||
max-width: 180px;
|
||||
min-width: 80px;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.8125rem;
|
||||
position: relative;
|
||||
margin-right: 3px;
|
||||
color: ${(props) => props.theme.requestTabs.color};
|
||||
background: transparent;
|
||||
max-width: 150px;
|
||||
border: 1px solid transparent;
|
||||
padding: 6px 0;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.15s ease;
|
||||
margin-bottom: 3px;
|
||||
list-style: none;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
cursor: pointer;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
height: 38px;
|
||||
|
||||
margin-right: 6px;
|
||||
color: ${(props) => props.theme.requestTabs.color};
|
||||
background: ${(props) => props.theme.requestTabs.bg};
|
||||
border-radius: 0;
|
||||
|
||||
.tab-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
background: ${(props) => props.theme.requestTabs.bg};
|
||||
border-color: transparent;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
|
||||
}
|
||||
|
||||
&:nth-last-child(1) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&.has-overflow:not(:hover) .tab-name {
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
${(props) => props.theme.requestTabs.color} 0%,
|
||||
${(props) => props.theme.requestTabs.color} calc(100% - 12px),
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to right,
|
||||
${(props) => props.theme.requestTabs.color} 0%,
|
||||
${(props) => props.theme.requestTabs.color} calc(100% - 12px),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
&.has-overflow:hover .tab-name {
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
${(props) => props.theme.requestTabs.color} 0%,
|
||||
${(props) => props.theme.requestTabs.color} calc(100% - 8px),
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to right,
|
||||
${(props) => props.theme.requestTabs.color} 0%,
|
||||
${(props) => props.theme.requestTabs.color} calc(100% - 8px),
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.bg || '#ffffff'};
|
||||
border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
border-bottom-color: ${(props) => props.theme.bg || '#ffffff'};
|
||||
border-radius: 8px 8px 0 0;
|
||||
z-index: 2;
|
||||
margin-bottom: -2px;
|
||||
padding-bottom: 12px;
|
||||
background: ${(props) => props.theme.requestTabs.active.bg};
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: -8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: transparent;
|
||||
border-bottom-right-radius: 8px;
|
||||
box-shadow: 2px 2px 0 0 ${(props) => props.theme.bg || '#ffffff'};
|
||||
border-right: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
&.active {
|
||||
.close-icon-container .close-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
right: -8px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: transparent;
|
||||
border-bottom-left-radius: 8px;
|
||||
box-shadow: -2px 2px 0 0 ${(props) => props.theme.bg || '#ffffff'};
|
||||
border-left: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
&:hover {
|
||||
.close-icon-container .close-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.short-tab {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
padding: 5px 0;
|
||||
vertical-align: bottom;
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
max-width: 34px;
|
||||
padding: 3px 0px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: ${(props) => props.theme.requestTabs.shortTab.color};
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
flex-shrink: 0;
|
||||
background-color: ${(props) => props.theme.requestTabs.shortTab.bg};
|
||||
position: relative;
|
||||
top: -1px;
|
||||
|
||||
> div {
|
||||
padding: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
transition: background-color 0.12s ease, color 0.12s ease;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
> div.home-icon-container {
|
||||
@@ -182,23 +81,19 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> div {
|
||||
background-color: ${(props) => props.theme.requestTabs.shortTab.hoverBg};
|
||||
color: ${(props) => props.theme.requestTabs.shortTab.hoverColor};
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-chevrons ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
@@ -10,15 +10,11 @@ import CollectionToolBar from './CollectionToolBar';
|
||||
import RequestTab from './RequestTab';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import DraggableTab from './DraggableTab';
|
||||
import CreateUntitledRequest from 'components/CreateUntitledRequest';
|
||||
|
||||
const RequestTabs = () => {
|
||||
const dispatch = useDispatch();
|
||||
const tabsRef = useRef();
|
||||
const scrollContainerRef = useRef();
|
||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||
const [tabOverflowStates, setTabOverflowStates] = useState({});
|
||||
const [showChevrons, setShowChevrons] = useState(false);
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
@@ -26,48 +22,10 @@ const RequestTabs = () => {
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const screenWidth = useSelector((state) => state.app.screenWidth);
|
||||
|
||||
const createSetHasOverflow = useCallback((tabUid) => {
|
||||
return (hasOverflow) => {
|
||||
setTabOverflowStates((prev) => {
|
||||
if (prev[tabUid] === hasOverflow) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[tabUid]: hasOverflow
|
||||
};
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const activeCollection = find(collections, (c) => c.uid === activeTab?.collectionUid);
|
||||
const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab?.collectionUid);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeTabUid || !activeTab) return;
|
||||
|
||||
const checkOverflow = () => {
|
||||
if (tabsRef.current && scrollContainerRef.current) {
|
||||
const hasOverflow = tabsRef.current.scrollWidth > scrollContainerRef.current.clientWidth;
|
||||
setShowChevrons(hasOverflow);
|
||||
}
|
||||
};
|
||||
|
||||
checkOverflow();
|
||||
const resizeObserver = new ResizeObserver(checkOverflow);
|
||||
if (scrollContainerRef.current) {
|
||||
resizeObserver.observe(scrollContainerRef.current);
|
||||
}
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [activeTabUid, activeTab, collectionRequestTabs.length, screenWidth, leftSidebarWidth, sidebarCollapsed]);
|
||||
|
||||
const getTabClassname = (tab, index) => {
|
||||
return classnames('request-tab select-none', {
|
||||
'active': tab.uid === activeTabUid,
|
||||
'last-tab': tabs && tabs.length && index === tabs.length - 1,
|
||||
'has-overflow': tabOverflowStates[tab.uid]
|
||||
'last-tab': tabs && tabs.length && index === tabs.length - 1
|
||||
});
|
||||
};
|
||||
|
||||
@@ -79,26 +37,37 @@ const RequestTabs = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const createNewTab = () => setNewRequestModalOpen(true);
|
||||
|
||||
if (!activeTabUid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!activeTab) {
|
||||
return <StyledWrapper>Something went wrong!</StyledWrapper>;
|
||||
}
|
||||
|
||||
const activeCollection = find(collections, (c) => c.uid === activeTab.collectionUid);
|
||||
const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab.collectionUid);
|
||||
|
||||
const effectiveSidebarWidth = sidebarCollapsed ? 0 : leftSidebarWidth;
|
||||
const maxTablistWidth = screenWidth - effectiveSidebarWidth - 150;
|
||||
const tabsWidth = collectionRequestTabs.length * 150 + 34; // 34: (+)icon
|
||||
const showChevrons = maxTablistWidth < tabsWidth;
|
||||
|
||||
const leftSlide = () => {
|
||||
scrollContainerRef.current?.scrollBy({
|
||||
tabsRef.current.scrollBy({
|
||||
left: -120,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
|
||||
// todo: bring new tab to focus if its not in focus
|
||||
// tabsRef.current.scrollLeft
|
||||
|
||||
const rightSlide = () => {
|
||||
scrollContainerRef.current?.scrollBy({
|
||||
tabsRef.current.scrollBy({
|
||||
left: 120,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
@@ -118,7 +87,7 @@ const RequestTabs = () => {
|
||||
{collectionRequestTabs && collectionRequestTabs.length ? (
|
||||
<>
|
||||
<CollectionToolBar collection={activeCollection} />
|
||||
<div className="flex items-center pl-2">
|
||||
<div className="flex items-center pl-4">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
@@ -134,40 +103,36 @@ const RequestTabs = () => {
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
<div className="tabs-scroll-container" style={{ maxWidth: maxTablistWidth }} ref={scrollContainerRef}>
|
||||
<ul role="tablist" ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<DraggableTab
|
||||
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<DraggableTab
|
||||
key={tab.uid}
|
||||
id={tab.uid}
|
||||
index={index}
|
||||
onMoveTab={(source, target) => {
|
||||
dispatch(reorderTabs({
|
||||
sourceUid: source,
|
||||
targetUid: target
|
||||
}));
|
||||
}}
|
||||
className={getTabClassname(tab, index)}
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab
|
||||
collectionRequestTabs={collectionRequestTabs}
|
||||
tabIndex={index}
|
||||
key={tab.uid}
|
||||
id={tab.uid}
|
||||
index={index}
|
||||
onMoveTab={(source, target) => {
|
||||
dispatch(reorderTabs({
|
||||
sourceUid: source,
|
||||
targetUid: target
|
||||
}));
|
||||
}}
|
||||
className={getTabClassname(tab, index)}
|
||||
onClick={() => handleClick(tab)}
|
||||
>
|
||||
<RequestTab
|
||||
collectionRequestTabs={collectionRequestTabs}
|
||||
tabIndex={index}
|
||||
key={tab.uid}
|
||||
tab={tab}
|
||||
collection={activeCollection}
|
||||
folderUid={tab.folderUid}
|
||||
hasOverflow={tabOverflowStates[tab.uid]}
|
||||
setHasOverflow={createSetHasOverflow(tab.uid)}
|
||||
/>
|
||||
</DraggableTab>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
</div>
|
||||
tab={tab}
|
||||
collection={activeCollection}
|
||||
folderUid={tab.folderUid}
|
||||
/>
|
||||
</DraggableTab>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
@@ -177,16 +142,19 @@ const RequestTabs = () => {
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
<div className="flex items-center short-tab">
|
||||
|
||||
{activeCollection && (
|
||||
<CreateUntitledRequest
|
||||
collectionUid={activeCollection.uid}
|
||||
itemUid={null}
|
||||
placement="bottom-start"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<li className="select-none short-tab" id="create-new-tab" onClick={createNewTab}>
|
||||
<div className="flex items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</li>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab choose-request">
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -6,7 +6,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -20,7 +20,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const ClearTimeline = ({ collection, item }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex items-center">
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button onClick={clearResponse} className="text-link hover:underline" title="Clear Timeline">
|
||||
Clear Timeline
|
||||
</button>
|
||||
|
||||
@@ -37,7 +37,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -51,7 +51,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -25,7 +25,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import GrpcStatusCode from './GrpcStatusCode';
|
||||
import ResponseTime from '../ResponseTime/index';
|
||||
import Timeline from '../Timeline';
|
||||
import ClearTimeline from '../ClearTimeline';
|
||||
import ResponseSave from '../ResponseSave';
|
||||
import ResponseClear from '../ResponseClear';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseTrailers from './ResponseTrailers';
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
|
||||
.query-response-content {
|
||||
border-top: 1px solid ${(props) => props.theme.console.border};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,60 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import QueryResult from '../QueryResult';
|
||||
import { useInitialResponseFormat, useResponsePreviewFormatOptions } from '../QueryResult/index';
|
||||
import QueryResultTypeSelector from '../QueryResult/QueryResultTypeSelector/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const QueryResponse = ({
|
||||
item,
|
||||
collection,
|
||||
data,
|
||||
dataBuffer,
|
||||
disableRunEventListener,
|
||||
headers,
|
||||
error
|
||||
}) => {
|
||||
const { initialFormat, initialTab } = useInitialResponseFormat(dataBuffer, headers);
|
||||
const previewFormatOptions = useResponsePreviewFormatOptions(dataBuffer, headers);
|
||||
const [selectedFormat, setSelectedFormat] = useState('raw');
|
||||
const [selectedTab, setSelectedTab] = useState('editor');
|
||||
|
||||
useEffect(() => {
|
||||
if (initialFormat !== null && initialTab !== null) {
|
||||
setSelectedFormat(initialFormat);
|
||||
setSelectedTab(initialTab);
|
||||
}
|
||||
}, [initialFormat, initialTab]);
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center justify-end p-2">
|
||||
<QueryResultTypeSelector
|
||||
formatOptions={previewFormatOptions}
|
||||
formatValue={selectedFormat}
|
||||
onFormatChange={(newFormat) => {
|
||||
setSelectedFormat(newFormat);
|
||||
}}
|
||||
onPreviewTabSelect={() => {
|
||||
setSelectedTab((prev) => prev === 'editor' ? 'preview' : 'editor');
|
||||
}}
|
||||
selectedTab={selectedTab}
|
||||
/>
|
||||
</div>
|
||||
<div className={classnames('flex-1 query-response-content', selectedTab === 'editor' ? 'px-2 py-1' : '')}>
|
||||
<QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
data={data}
|
||||
dataBuffer={dataBuffer}
|
||||
disableRunEventListener={disableRunEventListener}
|
||||
headers={headers}
|
||||
error={error}
|
||||
selectedFormat={selectedFormat}
|
||||
selectedTab={selectedTab}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryResponse;
|
||||
@@ -1,78 +0,0 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { isValidHtml } from 'utils/common/index';
|
||||
import { escapeHtml, isValidHtmlSnippet } from 'utils/response/index';
|
||||
|
||||
const HtmlPreview = React.memo(({ data, baseUrl }) => {
|
||||
const webviewContainerRef = useRef(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!webviewContainerRef.current) return;
|
||||
|
||||
const checkDragging = () => {
|
||||
const hasDraggingParent = webviewContainerRef.current?.closest('.dragging');
|
||||
setIsDragging(!!hasDraggingParent);
|
||||
};
|
||||
|
||||
// Watch from a common ancestor where .dragging gets added
|
||||
const watchTarget = webviewContainerRef.current.closest('.main-section')
|
||||
|| document.body;
|
||||
|
||||
const mutationObserver = new MutationObserver(checkDragging);
|
||||
mutationObserver.observe(watchTarget, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Check initial state
|
||||
checkDragging();
|
||||
|
||||
return () => mutationObserver.disconnect();
|
||||
}, []);
|
||||
|
||||
if (isValidHtml(data) || isValidHtmlSnippet(data)) {
|
||||
const htmlContent = data.includes('<head>')
|
||||
? data.replace('<head>', `<head><base href="${escapeHtml(baseUrl)}">`)
|
||||
: `<head><base href="${escapeHtml(baseUrl)}"></head>${data}`;
|
||||
|
||||
const dragStyles = isDragging ? { pointerEvents: 'none', userSelect: 'none' } : {};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={webviewContainerRef}
|
||||
className="h-full bg-white webview-container"
|
||||
style={dragStyles}
|
||||
>
|
||||
<webview
|
||||
src={`data:text/html; charset=utf-8,${encodeURIComponent(htmlContent)}`}
|
||||
webpreferences="disableDialogs=true, javascript=yes"
|
||||
className="h-full bg-white"
|
||||
style={dragStyles}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// For all other data types, render safely as formatted text
|
||||
let displayContent = '';
|
||||
if (data === null || data === undefined) {
|
||||
displayContent = String(data);
|
||||
} else if (typeof data === 'object') {
|
||||
displayContent = JSON.stringify(data, null);
|
||||
} else if (typeof data === 'string') {
|
||||
displayContent = data;
|
||||
} else {
|
||||
displayContent = String(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<pre
|
||||
className="bg-white font-mono text-[13px] whitespace-pre-wrap break-words overflow-auto overflow-x-hidden p-4 text-[#24292f] w-full max-w-full h-full box-border relative"
|
||||
>
|
||||
{displayContent}
|
||||
</pre>
|
||||
);
|
||||
});
|
||||
|
||||
export default HtmlPreview;
|
||||
@@ -1,63 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactJson from 'react-json-view';
|
||||
import ErrorAlert from 'ui/ErrorAlert/index';
|
||||
|
||||
const JsonPreview = ({ data, displayedTheme }) => {
|
||||
// Helper function to validate and parse JSON data
|
||||
const validateJsonData = (data) => {
|
||||
// If data is already an object or array, use it directly
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
return { data, error: null };
|
||||
}
|
||||
|
||||
// If data is a string, try to parse it
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
return { data: parsed, error: null };
|
||||
} catch (e) {
|
||||
return { data: null, error: `Invalid JSON format: ${e.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
// For other types, return error
|
||||
return { data: null, error: 'Invalid input. Expected a JSON object, array, or valid JSON string.' };
|
||||
};
|
||||
|
||||
// Validate and parse JSON data
|
||||
const jsonData = validateJsonData(data);
|
||||
|
||||
// Show error if parsing failed
|
||||
if (jsonData.error) {
|
||||
return <ErrorAlert title="Cannot preview as JSON" message={jsonData.error} />;
|
||||
}
|
||||
|
||||
// Validate that data can be rendered as JSON tree
|
||||
if (jsonData.data === null || jsonData.data === undefined) {
|
||||
return <ErrorAlert title="Cannot preview as JSON" message="Data is null or undefined. Expected a valid JSON object or array." />;
|
||||
}
|
||||
|
||||
if (typeof jsonData.data !== 'object') {
|
||||
return <ErrorAlert title="Cannot preview as JSON" message="Data cannot be rendered as a JSON tree. Expected a JSON object or array." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactJson
|
||||
src={jsonData.data}
|
||||
theme={displayedTheme === 'light' ? 'rjv-default' : 'monokai'}
|
||||
collapsed={1}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={true}
|
||||
enableClipboard={true}
|
||||
name={false}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace',
|
||||
padding: '16px'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default JsonPreview;
|
||||
@@ -1,25 +0,0 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
|
||||
const TextPreview = memo(({ data }) => {
|
||||
const displayData = useMemo(() => {
|
||||
if (data === null || data === undefined) {
|
||||
return String(data);
|
||||
}
|
||||
if (typeof data === 'object') {
|
||||
try {
|
||||
return JSON.stringify(data);
|
||||
} catch {
|
||||
return String(data);
|
||||
}
|
||||
}
|
||||
return String(data);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className="p-4 font-mono text-[13px] whitespace-pre-wrap break-words overflow-auto overflow-x-hidden w-full max-w-full h-full">
|
||||
{displayData}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default TextPreview;
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ReactPlayer from 'react-player';
|
||||
|
||||
const VideoPreview = React.memo(({ contentType, dataBuffer }) => {
|
||||
const [videoUrl, setVideoUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const videoType = contentType.split(';')[0];
|
||||
const byteArray = Buffer.from(dataBuffer, 'base64');
|
||||
const blob = new Blob([byteArray], { type: videoType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
setVideoUrl(url);
|
||||
return () => URL.revokeObjectURL(url);
|
||||
}, [contentType, dataBuffer]);
|
||||
|
||||
if (!videoUrl) return <div>Loading video...</div>;
|
||||
|
||||
return (
|
||||
<ReactPlayer
|
||||
url={videoUrl}
|
||||
controls
|
||||
muted={true}
|
||||
width="100%"
|
||||
height="100%"
|
||||
onError={(e) => console.error('Error loading video:', e)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default VideoPreview;
|
||||
@@ -1,77 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
.xml-container {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
.xml-node-name {
|
||||
color: ${(props) => props.theme.codemirror.tokens.property};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.xml-separator {
|
||||
color: ${(props) => props.theme.codemirror.tokens.operator};
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.xml-value {
|
||||
color: ${(props) => props.theme.codemirror.tokens.string};
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.xml-empty-value {
|
||||
color: ${(props) => props.theme.codemirror.tokens.comment};
|
||||
}
|
||||
|
||||
.xml-count {
|
||||
color: ${(props) => props.theme.codemirror.tokens.comment};
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.xml-toggle-button {
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${(props) => props.theme.codemirror.tokens.atom};
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.console.buttonHoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.xml-array-toggle-button {
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${(props) => props.theme.codemirror.tokens.atom};
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.console.buttonHoverBg};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,396 +0,0 @@
|
||||
import ErrorAlert from 'ui/ErrorAlert/index';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
// The expected "data" prop must be an XML string.
|
||||
export default function XmlPreview({ data, defaultExpanded = true }) {
|
||||
// Parse XML string
|
||||
const parsedData = useMemo(() => {
|
||||
if (typeof data !== 'string') {
|
||||
return { error: 'Invalid input. Expected an XML string.' };
|
||||
}
|
||||
|
||||
const parsed = parseXMLString(data);
|
||||
if (parsed === null) {
|
||||
return { error: 'Failed to parse XML string. Invalid XML format.' };
|
||||
}
|
||||
return parsed;
|
||||
}, [data]);
|
||||
|
||||
// Check for parsing error
|
||||
if (parsedData && typeof parsedData === 'object' && parsedData.error) {
|
||||
return (
|
||||
<ErrorAlert title="Cannot preview as XML" message={parsedData.error} />
|
||||
);
|
||||
}
|
||||
|
||||
// Validate that data can be rendered as a tree
|
||||
const isValidTreeData = (data) => {
|
||||
if (data === null || data === undefined) return false;
|
||||
if (typeof data === 'object' && !Array.isArray(data)) return true;
|
||||
if (Array.isArray(data)) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!isValidTreeData(parsedData)) {
|
||||
return (
|
||||
<ErrorAlert title="Cannot preview as XML" message="Data cannot be rendered as a tree. Expected a valid XML string." />
|
||||
);
|
||||
}
|
||||
|
||||
// If root is an object with a single key, unwrap it to show the actual root element
|
||||
let rootNode = parsedData;
|
||||
let rootNodeName = '';
|
||||
|
||||
if (typeof parsedData === 'object' && !Array.isArray(parsedData) && parsedData !== null) {
|
||||
const keys = Object.keys(parsedData).filter((k) => k !== '$' && k !== '@_' && k !== '#text');
|
||||
if (keys.length === 1) {
|
||||
rootNodeName = keys[0];
|
||||
rootNode = parsedData[keys[0]];
|
||||
} else if (keys.length === 0) {
|
||||
// Empty object with no children
|
||||
return (
|
||||
<ErrorAlert title="Cannot preview as XML" message="Cannot render XML tree. Root object is empty." />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="xml-container">
|
||||
<XmlNode
|
||||
node={rootNode}
|
||||
nodeName={rootNodeName}
|
||||
isRoot={true}
|
||||
isLast={true}
|
||||
defaultExpanded={defaultExpanded}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Component for rendering array entries with expand/collapse functionality
|
||||
const XmlArrayNode = ({ arrayKey, items, depth, defaultExpanded = true }) => {
|
||||
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||
|
||||
const toggle = (e) => {
|
||||
e.stopPropagation();
|
||||
setExpanded((v) => !v);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ paddingLeft: `${(depth + 1) * 20}px` }}>
|
||||
<div className="flex items-center mb-1">
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="xml-array-toggle-button"
|
||||
tabIndex={-1}
|
||||
aria-expanded={expanded}
|
||||
>
|
||||
{expanded ? '▼' : '▶'}
|
||||
</button>
|
||||
<span className="xml-node-name">{arrayKey}</span>
|
||||
<span className="xml-count">[{items.length}]</span>
|
||||
</div>
|
||||
{expanded && (
|
||||
<div className="array-content">
|
||||
{items.map((item, itemIdx) => (
|
||||
<XmlNode
|
||||
key={`${arrayKey}-${itemIdx}`}
|
||||
node={item}
|
||||
nodeName={`${itemIdx}`}
|
||||
isLast={itemIdx === items.length - 1}
|
||||
defaultExpanded={false}
|
||||
depth={depth + 2}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const XmlNode = ({
|
||||
node,
|
||||
nodeName = '',
|
||||
isRoot = false,
|
||||
isLast = true,
|
||||
defaultExpanded = true,
|
||||
depth = 0
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||
|
||||
let displayNodeName = nodeName;
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
// For repeated XML elements with same name (e.g. <item>...</item><item>...</item>)
|
||||
return (
|
||||
<>
|
||||
{node.map((item, idx) => (
|
||||
<XmlNode
|
||||
key={idx}
|
||||
node={item}
|
||||
nodeName={displayNodeName}
|
||||
isRoot={false}
|
||||
isLast={idx === node.length - 1}
|
||||
defaultExpanded={false}
|
||||
depth={depth}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const childEntries = getChildrenEntries(node);
|
||||
const childCount = getChildCount(node);
|
||||
const isLeaf = isTextNode(node) || (typeof node === 'object' && childCount === 0);
|
||||
|
||||
const toggle = (e) => {
|
||||
e.stopPropagation();
|
||||
setExpanded((v) => !v);
|
||||
};
|
||||
|
||||
// For leaf nodes with text content or attributes with empty values
|
||||
if (isLeaf && isTextNode(node)) {
|
||||
const value = String(node);
|
||||
|
||||
return (
|
||||
<div className="flex items-start mb-1" style={{ paddingLeft: `${depth * 20}px` }}>
|
||||
{displayNodeName && (
|
||||
<>
|
||||
<span className="xml-node-name">{displayNodeName}</span>
|
||||
<span className="xml-separator">:</span>
|
||||
</>
|
||||
)}
|
||||
<span className="xml-value">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// For empty leaf nodes (attributes without values, etc)
|
||||
if (isLeaf && !isTextNode(node)) {
|
||||
// Check if this is an attribute-only node with _text
|
||||
if (typeof node === 'object' && node !== null && '_text' in node) {
|
||||
// This node has both attributes and text, handle in expandable section
|
||||
// Fall through to expandable node rendering
|
||||
} else {
|
||||
return (
|
||||
<div className="flex items-center mb-1" style={{ paddingLeft: `${depth * 20}px` }}>
|
||||
{displayNodeName && (
|
||||
<>
|
||||
<span className="xml-node-name">{displayNodeName}</span>
|
||||
<span className="xml-separator">:</span>
|
||||
<span className="xml-empty-value">{'{}'}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// For expandable nodes - show as tree structure
|
||||
// If no node name at root level, render children directly
|
||||
if (!displayNodeName && depth === 0) {
|
||||
if (childEntries.length > 0) {
|
||||
return (
|
||||
<div>
|
||||
{childEntries.map(([key, value], idx) => (
|
||||
<XmlNode
|
||||
key={key + idx}
|
||||
node={value}
|
||||
nodeName={key}
|
||||
isLast={idx === childEntries.length - 1}
|
||||
defaultExpanded={defaultExpanded}
|
||||
depth={0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// If no display name at non-root level, use a fallback
|
||||
if (!displayNodeName) {
|
||||
displayNodeName = '(unnamed)';
|
||||
}
|
||||
|
||||
// Determine if this node's value is an array
|
||||
const hasArrayValue = Array.isArray(node);
|
||||
const arrayLength = hasArrayValue ? node.length : 0;
|
||||
|
||||
return (
|
||||
<div style={{ paddingLeft: `${depth * 20}px` }}>
|
||||
<div className="flex items-center mb-1">
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="xml-toggle-button"
|
||||
tabIndex={-1}
|
||||
aria-expanded={expanded}
|
||||
>
|
||||
{expanded ? '▼' : '▶'}
|
||||
</button>
|
||||
|
||||
<span className="xml-node-name">
|
||||
{displayNodeName}
|
||||
</span>
|
||||
|
||||
{childCount > 0 && (
|
||||
<span className="xml-count">
|
||||
{`{${childCount}}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{expanded && childEntries.length > 0 && (
|
||||
<div>
|
||||
{childEntries.map(([key, value], idx) => {
|
||||
// Check if this is an attribute (starts with _)
|
||||
const isAttribute = key.startsWith('_');
|
||||
|
||||
// Handle attributes
|
||||
if (isAttribute) {
|
||||
const displayValue = value === '' ? 'value' : value;
|
||||
|
||||
return (
|
||||
<div key={key + idx} className="flex items-start mb-1" style={{ paddingLeft: `${(depth + 1) * 20}px` }}>
|
||||
<span className="xml-node-name">{key}</span>
|
||||
<span className="xml-separator">:</span>
|
||||
<span className={value === '' ? 'xml-empty-value' : 'xml-value'}>{displayValue}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if this child is an array
|
||||
const isArrayChild = Array.isArray(value);
|
||||
|
||||
if (isArrayChild) {
|
||||
return (
|
||||
<XmlArrayNode
|
||||
key={`${key}-${idx}`}
|
||||
arrayKey={key}
|
||||
items={value}
|
||||
depth={depth}
|
||||
defaultExpanded={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<XmlNode
|
||||
key={key + idx}
|
||||
node={value}
|
||||
nodeName={key}
|
||||
isLast={idx === childEntries.length - 1}
|
||||
defaultExpanded={false}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper function to parse XML string to object
|
||||
function parseXMLString(xmlString) {
|
||||
if (typeof xmlString !== 'string') return null;
|
||||
|
||||
try {
|
||||
const parser = new DOMParser();
|
||||
// Parse as XML only
|
||||
const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
|
||||
|
||||
// Check for parsing errors
|
||||
const parserError = xmlDoc.querySelector('parsererror');
|
||||
if (parserError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert XML DOM to object
|
||||
function xmlToObject(node) {
|
||||
if (node.nodeType !== 1) return null; // Not an element node
|
||||
|
||||
const result = {};
|
||||
|
||||
// Get attributes - store them directly with underscore prefix
|
||||
if (node.attributes && node.attributes.length > 0) {
|
||||
for (let i = 0; i < node.attributes.length; i++) {
|
||||
const attr = node.attributes[i];
|
||||
result[`_${attr.name}`] = attr.value || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Get child nodes
|
||||
const childNodes = Array.from(node.childNodes);
|
||||
const elementChildren = childNodes.filter((child) => child.nodeType === 1);
|
||||
const textChildren = childNodes.filter((child) => child.nodeType === 3 && child.textContent.trim());
|
||||
|
||||
// If only text children and no element children, return text content
|
||||
if (elementChildren.length === 0 && textChildren.length > 0) {
|
||||
const textContent = textChildren.map((t) => t.textContent.trim()).join(' ').trim();
|
||||
// If has attributes, store text as a special property
|
||||
if (Object.keys(result).length > 0) {
|
||||
result['_text'] = textContent;
|
||||
return result;
|
||||
}
|
||||
return textContent || null;
|
||||
}
|
||||
|
||||
// Process element children
|
||||
if (elementChildren.length > 0) {
|
||||
const childMap = {};
|
||||
elementChildren.forEach((child) => {
|
||||
const childName = child.nodeName; // Preserve original casing
|
||||
const childValue = xmlToObject(child);
|
||||
|
||||
if (childValue !== null || elementChildren.filter((c) => c.nodeName.toLowerCase() === childName).length > 1) {
|
||||
if (childMap[childName]) {
|
||||
// Multiple children with same name - convert to array
|
||||
if (!Array.isArray(childMap[childName])) {
|
||||
childMap[childName] = [childMap[childName]];
|
||||
}
|
||||
childMap[childName].push(childValue);
|
||||
} else {
|
||||
childMap[childName] = childValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Merge children into result
|
||||
Object.assign(result, childMap);
|
||||
}
|
||||
|
||||
return Object.keys(result).length > 0 ? result : null;
|
||||
}
|
||||
|
||||
const rootElement = xmlDoc.documentElement;
|
||||
if (!rootElement) return null;
|
||||
|
||||
const parsed = xmlToObject(rootElement);
|
||||
return parsed ? { [rootElement.nodeName]: parsed } : null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isTextNode(node) {
|
||||
return typeof node === 'string' || typeof node === 'number' || node === null;
|
||||
}
|
||||
|
||||
function getChildrenEntries(node) {
|
||||
// Given an XML-like JS object, return an array of [key, value] for all properties
|
||||
// This includes attributes (with _ prefix) and child elements
|
||||
if (typeof node !== 'object' || node === null) return [];
|
||||
return Object.entries(node);
|
||||
}
|
||||
|
||||
function getChildCount(node) {
|
||||
if (Array.isArray(node)) {
|
||||
return node.length;
|
||||
}
|
||||
const children = getChildrenEntries(node);
|
||||
return children.length;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import { get } from 'lodash';
|
||||
import find from 'lodash/find';
|
||||
@@ -11,22 +11,44 @@ import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
import 'react-pdf/dist/esm/Page/TextLayer.css';
|
||||
import { GlobalWorkerOptions } from 'pdfjs-dist/build/pdf';
|
||||
GlobalWorkerOptions.workerSrc = 'pdfjs-dist/legacy/build/pdf.worker.min.mjs';
|
||||
import XmlPreview from './XmlPreview/index';
|
||||
import TextPreview from './TextPreview';
|
||||
import HtmlPreview from './HtmlPreview';
|
||||
import VideoPreview from './VideoPreview';
|
||||
import JsonPreview from './JsonPreview';
|
||||
import ReactPlayer from 'react-player';
|
||||
|
||||
const VideoPreview = React.memo(({ contentType, dataBuffer }) => {
|
||||
const [videoUrl, setVideoUrl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const videoType = contentType.split(';')[0];
|
||||
const byteArray = Buffer.from(dataBuffer, 'base64');
|
||||
const blob = new Blob([byteArray], { type: videoType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
setVideoUrl(url);
|
||||
return () => URL.revokeObjectURL(url);
|
||||
}, [contentType, dataBuffer]);
|
||||
|
||||
if (!videoUrl) return <div>Loading video...</div>;
|
||||
|
||||
return (
|
||||
<ReactPlayer
|
||||
url={videoUrl}
|
||||
controls
|
||||
muted={true}
|
||||
width="100%"
|
||||
height="100%"
|
||||
onError={(e) => console.error('Error loading video:', e)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const QueryResultPreview = ({
|
||||
selectedTab,
|
||||
previewTab,
|
||||
allowedPreviewModes,
|
||||
data,
|
||||
dataBuffer,
|
||||
formattedData,
|
||||
item,
|
||||
contentType,
|
||||
collection,
|
||||
codeMirrorMode,
|
||||
previewMode,
|
||||
mode,
|
||||
disableRunEventListener,
|
||||
displayedTheme
|
||||
}) => {
|
||||
@@ -41,6 +63,10 @@ const QueryResultPreview = ({
|
||||
function onDocumentLoadSuccess({ numPages }) {
|
||||
setNumPages(numPages);
|
||||
}
|
||||
// Fail safe, so we don't render anything with an invalid tab
|
||||
if (!allowedPreviewModes.find((previewMode) => previewMode?.uid == previewTab?.uid)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onRun = () => {
|
||||
if (disableRunEventListener) {
|
||||
@@ -61,31 +87,19 @@ const QueryResultPreview = ({
|
||||
);
|
||||
};
|
||||
|
||||
if (selectedTab === 'editor') {
|
||||
return (
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
onScroll={onScroll}
|
||||
value={formattedData}
|
||||
mode={codeMirrorMode}
|
||||
initialScroll={focusedTab.responsePaneScrollPosition || 0}
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
switch (previewMode) {
|
||||
switch (previewTab?.mode) {
|
||||
case 'preview-web': {
|
||||
const baseUrl = item.requestSent?.url || '';
|
||||
return <HtmlPreview data={data} baseUrl={baseUrl} />;
|
||||
const webViewSrc = data.replace('<head>', `<head><base href="${item.requestSent?.url || ''}">`);
|
||||
return (
|
||||
<webview
|
||||
src={`data:text/html; charset=utf-8,${encodeURIComponent(webViewSrc)}`}
|
||||
webpreferences="disableDialogs=true, javascript=yes"
|
||||
className="h-full bg-white"
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'preview-image': {
|
||||
return <img src={`data:${contentType.replace(/\;(.*)/, '')};base64,${dataBuffer}`} />;
|
||||
return <img src={`data:${contentType.replace(/\;(.*)/, '')};base64,${dataBuffer}`} className="mx-auto" />;
|
||||
}
|
||||
case 'preview-pdf': {
|
||||
return (
|
||||
@@ -106,29 +120,24 @@ const QueryResultPreview = ({
|
||||
case 'preview-video': {
|
||||
return <VideoPreview contentType={contentType} dataBuffer={dataBuffer} />;
|
||||
}
|
||||
case 'preview-json': {
|
||||
return <JsonPreview data={data} displayedTheme={displayedTheme} />;
|
||||
}
|
||||
|
||||
case 'preview-text': {
|
||||
return <TextPreview data={data} />;
|
||||
}
|
||||
|
||||
case 'preview-xml': {
|
||||
return <XmlPreview data={data} />;
|
||||
}
|
||||
|
||||
default:
|
||||
case 'raw': {
|
||||
return (
|
||||
<div className="p-4 flex flex-col items-center justify-center h-full text-center">
|
||||
<div className="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2">
|
||||
No Preview Available
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Sorry, no preview is available for this content type.
|
||||
</div>
|
||||
</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
theme={displayedTheme}
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
onScroll={onScroll}
|
||||
value={formattedData}
|
||||
mode={mode}
|
||||
initialScroll={focusedTab.responsePaneScrollPosition || 0}
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.active {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
.preview-response-tab-label {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconEye } from '@tabler/icons';
|
||||
import ButtonDropdown from 'ui/ButtonDropdown';
|
||||
import ToggleSwitch from 'components/ToggleSwitch';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryResultTypeSelector = ({
|
||||
formatOptions,
|
||||
formatValue,
|
||||
onFormatChange,
|
||||
onPreviewTabSelect,
|
||||
selectedTab
|
||||
}) => {
|
||||
const header = (
|
||||
<div className="flex items-center justify-between gap-3 py-[0.35rem] px-[0.6rem]">
|
||||
<span className="text-[0.8125rem] preview-response-tab-label">Preview</span>
|
||||
<ToggleSwitch
|
||||
isOn={selectedTab === 'preview'}
|
||||
handleToggle={(e) => {
|
||||
e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
onPreviewTabSelect();
|
||||
}}
|
||||
size="2xs"
|
||||
data-testid="preview-response-tab"
|
||||
title={selectedTab === 'preview' ? 'Turn off Preview Mode' : 'Turn on Preview Mode'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<ButtonDropdown
|
||||
label={formatValue}
|
||||
options={formatOptions}
|
||||
value={formatValue}
|
||||
onChange={onFormatChange}
|
||||
header={header}
|
||||
className="h-[20px] text-[11px]"
|
||||
data-testid="format-response-tab"
|
||||
suffix={selectedTab === 'preview' ? <IconEye size={14} strokeWidth={2} className="active mr-[2px]" /> : null}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryResultTypeSelector;
|
||||
@@ -1,8 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
grid-template-rows: 1.25rem 1fr;
|
||||
|
||||
/* This is a hack to force Codemirror to use all available space */
|
||||
> div {
|
||||
|
||||
@@ -1,34 +1,15 @@
|
||||
import { debounce } from 'lodash';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { formatResponse, getContentType } from 'utils/common';
|
||||
import { getEncoding } from 'utils/common/index';
|
||||
import { getDefaultResponseFormat } from 'utils/response';
|
||||
import LargeResponseWarning from '../LargeResponseWarning';
|
||||
import QueryResultFilter from './QueryResultFilter';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { getContentType, formatResponse } from 'utils/common';
|
||||
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
|
||||
import QueryResultPreview from './QueryResultPreview';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { detectContentTypeFromBuffer } from 'utils/response/index';
|
||||
|
||||
const PREVIEW_FORMAT_OPTIONS = [
|
||||
{
|
||||
// name: 'Structured',
|
||||
options: [
|
||||
{ label: 'JSON', value: 'json', codeMirrorMode: 'application/ld+json' },
|
||||
{ label: 'HTML', value: 'html', codeMirrorMode: 'xml' },
|
||||
{ label: 'XML', value: 'xml', codeMirrorMode: 'xml' },
|
||||
{ label: 'JavaScript', value: 'javascript', codeMirrorMode: 'javascript' }
|
||||
]
|
||||
},
|
||||
{
|
||||
// name: 'Raw',
|
||||
options: [
|
||||
{ label: 'Raw', value: 'raw', codeMirrorMode: 'text/plain' },
|
||||
{ label: 'Hex', value: 'hex', codeMirrorMode: 'text/plain' },
|
||||
{ label: 'Base64', value: 'base64', codeMirrorMode: 'text/plain' }
|
||||
]
|
||||
}
|
||||
];
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import { getEncoding, uuid } from 'utils/common/index';
|
||||
import LargeResponseWarning from '../LargeResponseWarning';
|
||||
|
||||
const formatErrorMessage = (error) => {
|
||||
if (!error) return 'Something went wrong';
|
||||
@@ -43,87 +24,9 @@ const formatErrorMessage = (error) => {
|
||||
return error;
|
||||
};
|
||||
|
||||
// Custom hook to determine the initial format and tab based on the data buffer and headers
|
||||
export const useInitialResponseFormat = (dataBuffer, headers) => {
|
||||
return useMemo(() => {
|
||||
let buffer = null;
|
||||
try {
|
||||
buffer = dataBuffer ? Buffer.from(dataBuffer, 'base64') : null;
|
||||
} catch (error) {
|
||||
console.error('Error converting dataBuffer to Buffer:', error);
|
||||
buffer = null;
|
||||
}
|
||||
|
||||
const detectedContentType = detectContentTypeFromBuffer(buffer);
|
||||
const contentType = getContentType(headers);
|
||||
|
||||
// Wait until both content types are available
|
||||
if (detectedContentType === null || contentType === undefined) {
|
||||
return { initialFormat: null, initialTab: null };
|
||||
}
|
||||
|
||||
const initial = getDefaultResponseFormat(contentType);
|
||||
return { initialFormat: initial.format, initialTab: initial.tab };
|
||||
}, [dataBuffer, headers]);
|
||||
};
|
||||
|
||||
// Custom hook to determine preview format options based on content type
|
||||
export const useResponsePreviewFormatOptions = (dataBuffer, headers) => {
|
||||
return useMemo(() => {
|
||||
let buffer = null;
|
||||
try {
|
||||
buffer = dataBuffer ? Buffer.from(dataBuffer, 'base64') : null;
|
||||
} catch (error) {
|
||||
console.error('Error converting dataBuffer to Buffer:', error);
|
||||
buffer = null;
|
||||
}
|
||||
|
||||
const detectedContentType = detectContentTypeFromBuffer(buffer);
|
||||
const contentType = getContentType(headers);
|
||||
|
||||
const byteFormatTypes = ['image', 'video', 'audio', 'pdf', 'zip'];
|
||||
|
||||
const isByteFormatType = (contentType) => {
|
||||
return byteFormatTypes.some((type) => contentType.includes(type));
|
||||
};
|
||||
|
||||
const getContentTypeToCheck = () => {
|
||||
if (detectedContentType) {
|
||||
return detectedContentType;
|
||||
}
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const contentTypeToCheck = getContentTypeToCheck();
|
||||
|
||||
if (contentTypeToCheck && isByteFormatType(contentTypeToCheck)) {
|
||||
return PREVIEW_FORMAT_OPTIONS.slice(1, 2); // Remove structured format options
|
||||
}
|
||||
|
||||
return PREVIEW_FORMAT_OPTIONS;
|
||||
}, [dataBuffer, headers]);
|
||||
};
|
||||
|
||||
const QueryResult = ({
|
||||
item,
|
||||
collection,
|
||||
data,
|
||||
dataBuffer,
|
||||
disableRunEventListener,
|
||||
headers,
|
||||
error,
|
||||
selectedFormat, // one of the options in PREVIEW_FORMAT_OPTIONS
|
||||
selectedTab // 'editor' or 'preview'
|
||||
}) => {
|
||||
let buffer = null;
|
||||
try {
|
||||
buffer = Buffer.from(dataBuffer, 'base64'); // dataBuffer is already a base64 string, convert it to actual Buffer
|
||||
} catch (error) {
|
||||
console.error('Error converting dataBuffer to Buffer:', error);
|
||||
buffer = null;
|
||||
}
|
||||
const detectedContentType = detectContentTypeFromBuffer(buffer);
|
||||
const QueryResult = ({ item, collection, data, dataBuffer, disableRunEventListener, headers, error }) => {
|
||||
const contentType = getContentType(headers);
|
||||
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
|
||||
const [filter, setFilter] = useState(null);
|
||||
const [showLargeResponse, setShowLargeResponse] = useState(false);
|
||||
const responseEncoding = getEncoding(headers);
|
||||
@@ -153,44 +56,65 @@ const QueryResult = ({
|
||||
if (isLargeResponse && !showLargeResponse) {
|
||||
return '';
|
||||
}
|
||||
return formatResponse(data, dataBuffer, selectedFormat, filter);
|
||||
return formatResponse(data, dataBuffer, mode, filter);
|
||||
},
|
||||
[data, dataBuffer, responseEncoding, selectedFormat, filter, isLargeResponse, showLargeResponse]
|
||||
[data, dataBuffer, responseEncoding, mode, filter, isLargeResponse, showLargeResponse]
|
||||
);
|
||||
|
||||
const debouncedResultFilterOnChange = debounce((e) => {
|
||||
setFilter(e.target.value);
|
||||
}, 250);
|
||||
|
||||
const previewMode = useMemo(() => {
|
||||
// Derive preview mode based on selected format
|
||||
if (selectedFormat === 'html') return 'preview-web';
|
||||
if (selectedFormat === 'json') return 'preview-json';
|
||||
if (selectedFormat === 'xml') return 'preview-xml';
|
||||
if (selectedFormat === 'raw') return 'preview-text';
|
||||
if (selectedFormat === 'javascript') return 'preview-web';
|
||||
const allowedPreviewModes = useMemo(() => {
|
||||
// Always show raw
|
||||
const allowedPreviewModes = [{ mode: 'raw', name: 'Raw', uid: uuid() }];
|
||||
|
||||
// For base64/hex, check content type to determine binary preview type
|
||||
if (selectedFormat === 'base64' || selectedFormat === 'hex') {
|
||||
if (detectedContentType) {
|
||||
if (detectedContentType.includes('image')) return 'preview-image';
|
||||
if (detectedContentType.includes('pdf')) return 'preview-pdf';
|
||||
if (detectedContentType.includes('audio')) return 'preview-audio';
|
||||
if (detectedContentType.includes('video')) return 'preview-video';
|
||||
}
|
||||
// for all other content types, return preview-text
|
||||
return 'preview-text';
|
||||
if (!mode || !contentType) return allowedPreviewModes;
|
||||
|
||||
if (mode?.includes('html') && typeof data === 'string') {
|
||||
allowedPreviewModes.unshift({ mode: 'preview-web', name: 'Web', uid: uuid() });
|
||||
} else if (mode.includes('image')) {
|
||||
allowedPreviewModes.unshift({ mode: 'preview-image', name: 'Image', uid: uuid() });
|
||||
} else if (contentType.includes('pdf')) {
|
||||
allowedPreviewModes.unshift({ mode: 'preview-pdf', name: 'PDF', uid: uuid() });
|
||||
} else if (contentType.includes('audio')) {
|
||||
allowedPreviewModes.unshift({ mode: 'preview-audio', name: 'Audio', uid: uuid() });
|
||||
} else if (contentType.includes('video')) {
|
||||
allowedPreviewModes.unshift({ mode: 'preview-video', name: 'Video', uid: uuid() });
|
||||
}
|
||||
return 'preview-text';
|
||||
}, [selectedFormat, detectedContentType]);
|
||||
|
||||
const codeMirrorMode = useMemo(() => {
|
||||
return PREVIEW_FORMAT_OPTIONS
|
||||
.flatMap((option) => option.options)
|
||||
.find((option) => option.value === selectedFormat)?.codeMirrorMode || 'text/plain';
|
||||
}, [selectedFormat]);
|
||||
return allowedPreviewModes;
|
||||
}, [mode, data, formattedData]);
|
||||
|
||||
const queryFilterEnabled = useMemo(() => codeMirrorMode.includes('json') && selectedFormat === 'json' && selectedTab === 'editor', [codeMirrorMode, selectedFormat, selectedTab]);
|
||||
const [previewTab, setPreviewTab] = useState(allowedPreviewModes[0]);
|
||||
// Ensure the active Tab is always allowed
|
||||
useEffect(() => {
|
||||
if (!allowedPreviewModes.find((previewMode) => previewMode?.uid == previewTab?.uid)) {
|
||||
setPreviewTab(allowedPreviewModes[0]);
|
||||
}
|
||||
}, [previewTab, allowedPreviewModes]);
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
if (allowedPreviewModes.length === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return allowedPreviewModes.map((previewMode) => (
|
||||
<div
|
||||
className={classnames(
|
||||
'select-none capitalize',
|
||||
previewMode?.uid === previewTab?.uid ? 'active' : 'cursor-pointer'
|
||||
)}
|
||||
role="tab"
|
||||
onClick={() => setPreviewTab(previewMode)}
|
||||
key={previewMode?.uid}
|
||||
>
|
||||
{previewMode?.name}
|
||||
</div>
|
||||
));
|
||||
}, [allowedPreviewModes, previewTab]);
|
||||
|
||||
const queryFilterEnabled = useMemo(() => mode.includes('json'), [mode]);
|
||||
const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;
|
||||
|
||||
return (
|
||||
@@ -198,6 +122,9 @@ const QueryResult = ({
|
||||
className="w-full h-full relative flex"
|
||||
queryFilterEnabled={queryFilterEnabled}
|
||||
>
|
||||
<div className="flex justify-end gap-2 text-xs" role="tablist">
|
||||
{tabs}
|
||||
</div>
|
||||
{error ? (
|
||||
<div>
|
||||
{hasScriptError ? null : (
|
||||
@@ -220,23 +147,21 @@ const QueryResult = ({
|
||||
) : (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 relative">
|
||||
<div className="absolute top-0 left-0 h-full w-full" data-testid="response-preview-container">
|
||||
<QueryResultPreview
|
||||
selectedTab={selectedTab}
|
||||
data={data}
|
||||
dataBuffer={dataBuffer}
|
||||
formattedData={formattedData}
|
||||
item={item}
|
||||
contentType={contentType}
|
||||
previewMode={previewMode}
|
||||
codeMirrorMode={codeMirrorMode}
|
||||
collection={collection}
|
||||
disableRunEventListener={disableRunEventListener}
|
||||
displayedTheme={displayedTheme}
|
||||
/>
|
||||
</div>
|
||||
<QueryResultPreview
|
||||
previewTab={previewTab}
|
||||
data={data}
|
||||
dataBuffer={dataBuffer}
|
||||
formattedData={formattedData}
|
||||
item={item}
|
||||
contentType={contentType}
|
||||
mode={mode}
|
||||
collection={collection}
|
||||
allowedPreviewModes={allowedPreviewModes}
|
||||
disableRunEventListener={disableRunEventListener}
|
||||
displayedTheme={displayedTheme}
|
||||
/>
|
||||
{queryFilterEnabled && (
|
||||
<QueryResultFilter filter={filter} onChange={debouncedResultFilterOnChange} mode={codeMirrorMode} />
|
||||
<QueryResultFilter filter={filter} onChange={debouncedResultFilterOnChange} mode={mode} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { IconDots } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseClear from 'src/components/ResponsePane/ResponseClear';
|
||||
import ResponseDownload from 'src/components/ResponsePane/ResponseDownload';
|
||||
import ResponseSave from 'src/components/ResponsePane/ResponseSave';
|
||||
|
||||
const ResponseActions = ({ collection, item }) => {
|
||||
const menuDropdownTippyRef = useRef();
|
||||
@@ -26,7 +26,7 @@ const ResponseActions = ({ collection, item }) => {
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement="bottom-end">
|
||||
<ResponseClear item={item} collection={collection} asDropdownItem onClose={handleClose} />
|
||||
<ResponseDownload item={item} asDropdownItem onClose={handleClose} />
|
||||
<ResponseSave item={item} asDropdownItem onClose={handleClose} />
|
||||
</Dropdown>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -3,13 +3,6 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${(props) => props.theme.codemirror.variable.info.iconColor};
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.workspace.button.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -24,51 +24,27 @@ const getTitleText = ({ isResponseTooLarge, isStreamingResponse }) => {
|
||||
return 'Save current response as example';
|
||||
};
|
||||
|
||||
const ResponseBookmark = ({ item, collection, responseSize, children }) => {
|
||||
const ResponseBookmark = ({ item, collection, responseSize }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [showSaveResponseExampleModal, setShowSaveResponseExampleModal] = useState(false);
|
||||
const response = item.response || {};
|
||||
|
||||
const isResponseTooLarge = responseSize >= 5 * 1024 * 1024; // 5 MB
|
||||
const isStreamingResponse = response.stream;
|
||||
const isDisabled = isResponseTooLarge || isStreamingResponse;
|
||||
|
||||
// Only show for HTTP requests
|
||||
if (item.type !== 'http-request') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (isDisabled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleSaveClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveClick = (e) => {
|
||||
const handleSaveClick = () => {
|
||||
if (!response || response.error) {
|
||||
toast.error('No valid response to save as example');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isResponseTooLarge) {
|
||||
toast.error('Response size exceeds 5MB limit. Cannot save as example.');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDisabled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -140,28 +116,21 @@ const ResponseBookmark = ({ item, collection, responseSize, children }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
role={!!children ? 'button' : undefined}
|
||||
tabIndex={isDisabled ? -1 : 0}
|
||||
aria-disabled={isDisabled}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={handleSaveClick}
|
||||
title={
|
||||
!children ? disabledMessage : (isDisabled ? disabledMessage : null)
|
||||
}
|
||||
className={classnames({
|
||||
'opacity-50 cursor-not-allowed': isDisabled
|
||||
})}
|
||||
data-testid="response-bookmark-btn"
|
||||
>
|
||||
{children ?? (
|
||||
<StyledWrapper className="flex items-center">
|
||||
<button className="p-1">
|
||||
<IconBookmark size={16} strokeWidth={2} />
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
)}
|
||||
</div>
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button
|
||||
onClick={handleSaveClick}
|
||||
disabled={isResponseTooLarge || isStreamingResponse}
|
||||
title={
|
||||
disabledMessage
|
||||
}
|
||||
className={classnames('p-1', {
|
||||
'opacity-50 cursor-not-allowed': isResponseTooLarge || isStreamingResponse
|
||||
})}
|
||||
data-testid="response-bookmark-btn"
|
||||
>
|
||||
<IconBookmark size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
|
||||
<CreateExampleModal
|
||||
isOpen={showSaveResponseExampleModal}
|
||||
|
||||
@@ -2,13 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
color: ${(props) => props.theme.codemirror.variable.info.iconColor};
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.workspace.button.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -4,11 +4,11 @@ import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { responseCleared } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
// Hook to get clear response function
|
||||
export const useResponseClear = (item, collection) => {
|
||||
const ResponseClear = ({ collection, item, asDropdownItem, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const clearResponse = () => {
|
||||
if (onClose) onClose();
|
||||
dispatch(
|
||||
responseCleared({
|
||||
itemUid: item.uid,
|
||||
@@ -18,29 +18,21 @@ export const useResponseClear = (item, collection) => {
|
||||
);
|
||||
};
|
||||
|
||||
return { clearResponse };
|
||||
};
|
||||
|
||||
const ResponseClear = ({ collection, item, children }) => {
|
||||
const { clearResponse } = useResponseClear(item, collection);
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
clearResponse();
|
||||
}
|
||||
};
|
||||
if (asDropdownItem) {
|
||||
return (
|
||||
<div className="dropdown-item" onClick={clearResponse}>
|
||||
<IconEraser size={16} strokeWidth={1.5} className="icon mr-2" />
|
||||
Clear
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div role={!!children ? 'button' : undefined} tabIndex={0} onClick={clearResponse} title={!children ? 'Clear response' : null} onKeyDown={handleKeyDown} data-testid="response-clear-button">
|
||||
{children ? children : (
|
||||
<StyledWrapper className="flex items-center">
|
||||
<button className="p-1">
|
||||
<IconEraser size={16} strokeWidth={2} />
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
)}
|
||||
</div>
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button onClick={clearResponse} title="Clear response">
|
||||
<IconEraser size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default ResponseClear;
|
||||
|
||||
@@ -2,13 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
color: ${(props) => props.theme.codemirror.variable.info.iconColor};
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.workspace.button.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -3,8 +3,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconCopy, IconCheck } from '@tabler/icons';
|
||||
|
||||
// Hook to get copy response function
|
||||
export const useResponseCopy = (item) => {
|
||||
const ResponseCopy = ({ item }) => {
|
||||
const response = item.response || {};
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
@@ -31,39 +30,16 @@ export const useResponseCopy = (item) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { copyResponse, copied, hasData: !!response.data };
|
||||
};
|
||||
|
||||
const ResponseCopy = ({ item, children }) => {
|
||||
const { copyResponse, copied, hasData } = useResponseCopy(item);
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && hasData) {
|
||||
e.preventDefault();
|
||||
copyResponse();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (hasData) {
|
||||
copyResponse();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div role={!!children ? 'button' : undefined} tabIndex={0} onClick={handleClick} title={!children ? 'Copy response to clipboard' : null} onKeyDown={handleKeyDown} data-testid="response-copy-btn">
|
||||
{children ? children : (
|
||||
<StyledWrapper className="flex items-center">
|
||||
<button className="p-1" disabled={!hasData}>
|
||||
{copied ? (
|
||||
<IconCheck size={16} strokeWidth={2} />
|
||||
) : (
|
||||
<IconCopy size={16} strokeWidth={2} />
|
||||
)}
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
)}
|
||||
</div>
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button onClick={copyResponse} disabled={!response.data} title="Copy response to clipboard">
|
||||
{copied ? (
|
||||
<IconCheck size={16} strokeWidth={1.5} />
|
||||
) : (
|
||||
<IconCopy size={16} strokeWidth={1.5} />
|
||||
)}
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
color: ${(props) => props.theme.codemirror.variable.info.iconColor};
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.workspace.button.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,63 +0,0 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import get from 'lodash/get';
|
||||
import { IconDownload } from '@tabler/icons';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const ResponseDownload = ({ item, children }) => {
|
||||
const { ipcRenderer } = window;
|
||||
const response = item.response || {};
|
||||
const isDisabled = !response.dataBuffer;
|
||||
|
||||
const saveResponseToFile = () => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-response-to-file', response, item?.requestSent?.url, item.pathname)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error(get(err, 'error.message') || 'Something went wrong!');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (isDisabled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
saveResponseToFile();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role={!!children ? 'button' : undefined}
|
||||
tabIndex={isDisabled ? -1 : 0}
|
||||
aria-disabled={isDisabled}
|
||||
onClick={saveResponseToFile}
|
||||
onKeyDown={handleKeyDown}
|
||||
title={!children ? 'Save response to file' : null}
|
||||
className={classnames({
|
||||
'opacity-50 cursor-not-allowed': isDisabled
|
||||
})}
|
||||
>
|
||||
{children ? children : (
|
||||
<StyledWrapper className="flex items-center">
|
||||
<button className="p-1">
|
||||
<IconDownload size={16} strokeWidth={2} />
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ResponseDownload;
|
||||
@@ -8,13 +8,7 @@ const Wrapper = styled.div`
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.codemirror.variable.info.iconColor};
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.workspace.button.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
export const IconDockToBottom = () => {
|
||||
const IconDockToBottom = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
>
|
||||
@@ -25,14 +25,14 @@ export const IconDockToBottom = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const IconDockToRight = () => {
|
||||
const IconDockToRight = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
>
|
||||
@@ -48,8 +48,7 @@ export const IconDockToRight = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Hook to get orientation and toggle function
|
||||
export const useResponseLayoutToggle = () => {
|
||||
const ResponseLayoutToggle = () => {
|
||||
const dispatch = useDispatch();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const orientation = preferences?.layout?.responsePaneOrientation || 'horizontal';
|
||||
@@ -66,42 +65,19 @@ export const useResponseLayoutToggle = () => {
|
||||
dispatch(savePreferences(updatedPreferences));
|
||||
};
|
||||
|
||||
return { orientation, toggleOrientation };
|
||||
};
|
||||
|
||||
const ResponseLayoutToggle = ({ children }) => {
|
||||
const { orientation, toggleOrientation } = useResponseLayoutToggle();
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleOrientation();
|
||||
}
|
||||
};
|
||||
|
||||
const title = !children ? (orientation === 'horizontal' ? 'Switch to vertical layout' : 'Switch to horizontal layout') : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={toggleOrientation}
|
||||
title={title}
|
||||
onKeyDown={handleKeyDown}
|
||||
data-testid="response-layout-toggle-button"
|
||||
>
|
||||
{children ? children : (
|
||||
<StyledWrapper className="flex items-center w-full">
|
||||
<button className="p-1">
|
||||
{orientation === 'horizontal' ? (
|
||||
<IconDockToBottom />
|
||||
) : (
|
||||
<IconDockToRight />
|
||||
)}
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
)}
|
||||
</div>
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button
|
||||
onClick={toggleOrientation}
|
||||
title={orientation === 'horizontal' ? 'Switch to vertical layout' : 'Switch to horizontal layout'}
|
||||
>
|
||||
{orientation === 'horizontal' ? (
|
||||
<IconDockToBottom />
|
||||
) : (
|
||||
<IconDockToRight />
|
||||
)}
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ describe('ResponseLayoutToggle', () => {
|
||||
describe('Initial Render', () => {
|
||||
it('should render with horizontal orientation by default', () => {
|
||||
renderWithProviders(<ResponseLayoutToggle />);
|
||||
const button = screen.getByTestId('response-layout-toggle-button');
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveAttribute('title', 'Switch to vertical layout');
|
||||
});
|
||||
@@ -100,7 +100,7 @@ describe('ResponseLayoutToggle', () => {
|
||||
}
|
||||
};
|
||||
renderWithProviders(<ResponseLayoutToggle />, customState);
|
||||
const button = screen.getByTestId('response-layout-toggle-button');
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveAttribute('title', 'Switch to horizontal layout');
|
||||
});
|
||||
@@ -109,7 +109,7 @@ describe('ResponseLayoutToggle', () => {
|
||||
describe('Interaction', () => {
|
||||
it('should switch to vertical layout when clicked in horizontal mode', () => {
|
||||
const { store } = renderWithProviders(<ResponseLayoutToggle />);
|
||||
const button = screen.getByTestId('response-layout-toggle-button');
|
||||
const button = screen.getByRole('button');
|
||||
|
||||
// Initial state check
|
||||
expect(button).toHaveAttribute('title', 'Switch to vertical layout');
|
||||
@@ -145,7 +145,7 @@ describe('ResponseLayoutToggle', () => {
|
||||
}
|
||||
};
|
||||
const { store } = renderWithProviders(<ResponseLayoutToggle />, customState);
|
||||
const button = screen.getByTestId('response-layout-toggle-button');
|
||||
const button = screen.getByRole('button');
|
||||
|
||||
// Initial state check
|
||||
expect(button).toHaveAttribute('title', 'Switch to horizontal layout');
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,188 +0,0 @@
|
||||
import React, { useState, useEffect, useRef, forwardRef, useCallback, useMemo } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import { IconDots, IconDownload, IconEraser, IconBookmark, IconCopy } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import ResponseDownload from '../ResponseDownload';
|
||||
import ResponseBookmark from '../ResponseBookmark';
|
||||
import ResponseClear from '../ResponseClear';
|
||||
import ResponseLayoutToggle, { useResponseLayoutToggle, IconDockToBottom, IconDockToRight } from '../ResponseLayoutToggle';
|
||||
import ResponseCopy from '../ResponseCopy/index';
|
||||
import StyledWrapper from '../StyledWrapper';
|
||||
|
||||
const PADDING = 48;
|
||||
|
||||
const StyledMenuIcon = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1.25rem;
|
||||
width: 1.5rem;
|
||||
border: 1px solid ${(props) => props.theme.workspace.border};
|
||||
color: ${(props) => props.theme.codemirror.variable.info.iconColor};
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.workspace.button.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
`;
|
||||
|
||||
const MenuIcon = forwardRef((props, ref) => (
|
||||
<StyledMenuIcon
|
||||
ref={ref}
|
||||
title="More actions"
|
||||
{...props}
|
||||
>
|
||||
<IconDots size={16} strokeWidth={1.5} />
|
||||
</StyledMenuIcon>
|
||||
));
|
||||
|
||||
MenuIcon.displayName = 'MenuIcon';
|
||||
|
||||
const ResponsePaneActions = ({ item, collection, responseSize }) => {
|
||||
const { orientation } = useResponseLayoutToggle();
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const actionsRef = useRef(null);
|
||||
const dropdownTippyRef = useRef();
|
||||
const individualButtonsWidthRef = useRef(null);
|
||||
const showMenuRef = useRef(showMenu);
|
||||
|
||||
const checkSpace = useCallback(() => {
|
||||
const actionsContainer = actionsRef.current?.parentElement;
|
||||
const rightSideContainer = actionsContainer?.closest('.right-side-container');
|
||||
|
||||
if (!actionsContainer || !rightSideContainer) return;
|
||||
|
||||
const currentActionsWidth = actionsContainer.offsetWidth || 0;
|
||||
|
||||
// Store individual buttons width when they're visible
|
||||
if (!showMenuRef.current && currentActionsWidth > 0) {
|
||||
individualButtonsWidthRef.current = currentActionsWidth;
|
||||
}
|
||||
|
||||
// Calculate siblings total width
|
||||
let siblingsTotalWidth = 0;
|
||||
let sibling = actionsContainer.previousElementSibling;
|
||||
while (sibling) {
|
||||
siblingsTotalWidth += sibling.offsetWidth || 0;
|
||||
sibling = sibling.previousElementSibling;
|
||||
}
|
||||
|
||||
const actionsWidth = individualButtonsWidthRef.current || currentActionsWidth;
|
||||
const requiredWidth = actionsWidth + siblingsTotalWidth + PADDING;
|
||||
const shouldShowMenu = rightSideContainer.offsetWidth < requiredWidth;
|
||||
|
||||
if (showMenuRef.current !== shouldShowMenu) {
|
||||
showMenuRef.current = shouldShowMenu;
|
||||
setShowMenu(shouldShowMenu);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const debouncedCheckSpace = useMemo(
|
||||
() => debounce(checkSpace, 50),
|
||||
[checkSpace]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
showMenuRef.current = showMenu;
|
||||
}, [showMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
checkSpace();
|
||||
|
||||
const rightSideContainer = actionsRef.current?.closest('.right-side-container');
|
||||
if (!rightSideContainer) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(debouncedCheckSpace);
|
||||
resizeObserver.observe(rightSideContainer);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
debouncedCheckSpace.cancel();
|
||||
};
|
||||
}, [item, debouncedCheckSpace]);
|
||||
|
||||
const onDropdownCreate = (ref) => {
|
||||
dropdownTippyRef.current = ref;
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
if (dropdownTippyRef.current) {
|
||||
dropdownTippyRef.current.hide();
|
||||
}
|
||||
};
|
||||
|
||||
if (item.type !== 'http-request') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper ref={actionsRef} className="flex items-center gap-2">
|
||||
{showMenu ? (
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<MenuIcon data-testid="response-actions-menu" />} placement="bottom-end">
|
||||
|
||||
{/* Response Copy */}
|
||||
<ResponseCopy item={item}>
|
||||
<div className="dropdown-item" onClick={closeDropdown}>
|
||||
<span className="dropdown-icon">
|
||||
<IconCopy size={16} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Copy response</span>
|
||||
</div>
|
||||
</ResponseCopy>
|
||||
|
||||
{/* Response Save as Example */}
|
||||
<ResponseBookmark item={item} collection={collection} responseSize={responseSize}>
|
||||
<div className="dropdown-item" onClick={closeDropdown}>
|
||||
<span className="dropdown-icon">
|
||||
<IconBookmark size={16} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Save response</span>
|
||||
</div>
|
||||
</ResponseBookmark>
|
||||
|
||||
{/* Response Download */}
|
||||
<ResponseDownload item={item}>
|
||||
<div className="dropdown-item" onClick={closeDropdown}>
|
||||
<span className="dropdown-icon">
|
||||
<IconDownload size={16} strokeWidth={1.5} />
|
||||
</span>
|
||||
Download response
|
||||
</div>
|
||||
</ResponseDownload>
|
||||
|
||||
{/* Response Clear */}
|
||||
<ResponseClear item={item} collection={collection}>
|
||||
<div className="dropdown-item" onClick={closeDropdown}>
|
||||
<span className="dropdown-icon">
|
||||
<IconEraser size={16} strokeWidth={1.5} />
|
||||
</span>
|
||||
Clear response
|
||||
</div>
|
||||
</ResponseClear>
|
||||
|
||||
{/* Response Layout Toggle */}
|
||||
<ResponseLayoutToggle>
|
||||
<div className="dropdown-item" onClick={closeDropdown}>
|
||||
<span className="dropdown-icon">
|
||||
{orientation === 'horizontal' ? <IconDockToBottom /> : <IconDockToRight />}
|
||||
</span>
|
||||
<span>Change layout</span>
|
||||
</div>
|
||||
</ResponseLayoutToggle>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<div className="flex items-center gap-[2px]">
|
||||
<ResponseCopy item={item} />
|
||||
<ResponseBookmark item={item} collection={collection} responseSize={responseSize} />
|
||||
<ResponseDownload item={item} />
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseLayoutToggle />
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponsePaneActions;
|
||||
@@ -0,0 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import get from 'lodash/get';
|
||||
import { IconDownload } from '@tabler/icons';
|
||||
|
||||
const ResponseSave = ({ item, asDropdownItem, onClose }) => {
|
||||
const { ipcRenderer } = window;
|
||||
const response = item.response || {};
|
||||
|
||||
const saveResponseToFile = () => {
|
||||
if (!response.dataBuffer) return;
|
||||
if (onClose) onClose();
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-response-to-file', response, item?.requestSent?.url, item.pathname)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error(get(err, 'error.message') || 'Something went wrong!');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (asDropdownItem) {
|
||||
return (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={saveResponseToFile}
|
||||
disabled={!response.dataBuffer}
|
||||
style={!response.dataBuffer ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
|
||||
>
|
||||
<IconDownload size={16} strokeWidth={1.5} className="icon mr-2" />
|
||||
Download
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button onClick={saveResponseToFile} disabled={!response.dataBuffer} title="Save response to file">
|
||||
<IconDownload size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default ResponseSave;
|
||||
@@ -19,7 +19,7 @@ const ResponseSize = ({ size }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper title={(size?.toLocaleString() || '0') + 'B'} className="ml-2">
|
||||
<StyledWrapper title={(size?.toLocaleString() || '0') + 'B'} className="ml-4">
|
||||
{sizeToDisplay}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -21,7 +21,7 @@ const ResponseStopWatch = ({ startMillis }) => {
|
||||
let seconds = milliseconds / 1000;
|
||||
let secondsFormatted = `${seconds.toFixed(1)}s`;
|
||||
let width = secondsFormatted.length * 0.4; // Calculate width manually to stop parent layout from "flickering" by changing width too fast
|
||||
return <StyledWrapper className="ml-2" style={{ width: `${width}rem` }}>{secondsFormatted}</StyledWrapper>;
|
||||
return <StyledWrapper className="ml-4" style={{ width: `${width}rem` }}>{secondsFormatted}</StyledWrapper>;
|
||||
};
|
||||
|
||||
export default React.memo(ResponseStopWatch);
|
||||
|
||||
@@ -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};
|
||||
`;
|
||||
|
||||
|
||||
@@ -17,6 +17,6 @@ const ResponseTime = ({ duration }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <StyledWrapper className="ml-2">{durationToDisplay}</StyledWrapper>;
|
||||
return <StyledWrapper className="ml-4">{durationToDisplay}</StyledWrapper>;
|
||||
};
|
||||
export default ResponseTime;
|
||||
|
||||
@@ -2,8 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
|
||||
&.text-ok {
|
||||
color: ${(props) => props.theme.requestTabPanel.responseOk};
|
||||
|
||||
@@ -6,7 +6,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
// Todo: text-error class is not getting pulled in for 500 errors
|
||||
const StatusCode = ({ status, statusText, isStreaming }) => {
|
||||
const getTabClassname = (status) => {
|
||||
return classnames({
|
||||
return classnames('ml-2', {
|
||||
'text-ok': status >= 100 && status < 200,
|
||||
'text-ok': status >= 200 && status < 300,
|
||||
'text-error': status >= 300 && status < 400,
|
||||
|
||||
@@ -6,7 +6,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -20,7 +20,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
@@ -34,12 +33,6 @@ const StyledWrapper = styled.div`
|
||||
.all-tests-passed {
|
||||
color: ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 16px;
|
||||
border-left: 1px solid ${(props) => props.theme.preferences.sidebar.border};
|
||||
margin: 0 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import QueryResponse from 'components/ResponsePane/QueryResponse/index';
|
||||
import QueryResult from 'components/ResponsePane/QueryResult/index';
|
||||
import { useState } from 'react';
|
||||
|
||||
const BodyBlock = ({ collection, data, dataBuffer, headers, error, item }) => {
|
||||
@@ -14,7 +14,7 @@ const BodyBlock = ({ collection, data, dataBuffer, headers, error, item }) => {
|
||||
<div className="mt-2">
|
||||
{data || dataBuffer ? (
|
||||
<div className="h-96 overflow-auto">
|
||||
<QueryResponse
|
||||
<QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
data={data}
|
||||
|
||||
@@ -11,7 +11,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -25,7 +25,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
@@ -16,11 +16,12 @@ import TestResultsLabel from './TestResultsLabel';
|
||||
import ScriptError from './ScriptError';
|
||||
import ScriptErrorIcon from './ScriptErrorIcon';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponsePaneActions from './ResponsePaneActions';
|
||||
import QueryResultTypeSelector from './QueryResult/QueryResultTypeSelector/index';
|
||||
import { useInitialResponseFormat, useResponsePreviewFormatOptions } from './QueryResult/index';
|
||||
import ResponseActions from 'src/components/ResponsePane/ResponseActions';
|
||||
import ResponseBookmark from 'src/components/ResponsePane/ResponseBookmark';
|
||||
import ResponseCopy from 'src/components/ResponsePane/ResponseCopy';
|
||||
import SkippedRequest from './SkippedRequest';
|
||||
import ClearTimeline from './ClearTimeline/index';
|
||||
import ResponseLayoutToggle from './ResponseLayoutToggle';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import ResponseStopWatch from 'components/ResponsePane/ResponseStopWatch';
|
||||
import WSMessagesList from './WsResponsePane/WSMessagesList';
|
||||
@@ -31,19 +32,6 @@ const ResponsePane = ({ item, collection }) => {
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
const [showScriptErrorCard, setShowScriptErrorCard] = useState(false);
|
||||
const [selectedFormat, setSelectedFormat] = useState('raw');
|
||||
const [selectedTab, setSelectedTab] = useState('editor');
|
||||
|
||||
// Initialize format and tab only once when data loads
|
||||
const { initialFormat, initialTab } = useInitialResponseFormat(item.response?.dataBuffer, item.response?.headers);
|
||||
const previewFormatOptions = useResponsePreviewFormatOptions(item.response?.dataBuffer, item.response?.headers);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialFormat !== null && initialTab !== null) {
|
||||
setSelectedFormat(initialFormat);
|
||||
setSelectedTab(initialTab);
|
||||
}
|
||||
}, [initialFormat, initialTab]);
|
||||
|
||||
const requestTimeline = ([...(collection.timeline || [])]).filter((obj) => {
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
@@ -98,8 +86,6 @@ const ResponsePane = ({ item, collection }) => {
|
||||
headers={response.headers}
|
||||
error={response.error}
|
||||
key={item.filename}
|
||||
selectedFormat={selectedFormat}
|
||||
selectedTab={selectedTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -171,7 +157,7 @@ const ResponsePane = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center px-4 tabs" role="tablist">
|
||||
<div className="flex flex-wrap items-center px-4 tabs" role="tablist">
|
||||
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
|
||||
Response
|
||||
</div>
|
||||
@@ -191,50 +177,33 @@ const ResponsePane = ({ item, collection }) => {
|
||||
/>
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center right-side-container">
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
{hasScriptError && !showScriptErrorCard && (
|
||||
<ScriptErrorIcon
|
||||
itemUid={item.uid}
|
||||
onClick={() => setShowScriptErrorCard(true)}
|
||||
/>
|
||||
)}
|
||||
{focusedTab?.responsePaneTab === 'response' ? (
|
||||
<ResponseLayoutToggle />
|
||||
{focusedTab?.responsePaneTab === 'timeline' ? (
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
) : (item?.response && !item?.response?.error) ? (
|
||||
<>
|
||||
<QueryResultTypeSelector
|
||||
formatOptions={previewFormatOptions}
|
||||
formatValue={selectedFormat}
|
||||
onFormatChange={(newFormat) => {
|
||||
setSelectedFormat(newFormat);
|
||||
}}
|
||||
onPreviewTabSelect={() => {
|
||||
setSelectedTab((prev) => prev === 'editor' ? 'preview' : 'editor');
|
||||
}}
|
||||
selectedTab={selectedTab}
|
||||
/>
|
||||
<div className="separator" />
|
||||
<ResponseBookmark item={item} collection={collection} responseSize={responseSize} />
|
||||
<ResponseCopy item={item} />
|
||||
<ResponseActions item={item} collection={collection} />
|
||||
<StatusCode status={response.status} isStreaming={item.response?.stream?.running} />
|
||||
{item.response?.stream?.running
|
||||
? <ResponseStopWatch startMillis={response.duration} />
|
||||
: <ResponseTime duration={response.duration} />}
|
||||
<ResponseSize size={responseSize} />
|
||||
</>
|
||||
) : null}
|
||||
<div className="flex items-center response-pane-status">
|
||||
<StatusCode status={response.status} isStreaming={item.response?.stream?.running} />
|
||||
{item.response?.stream?.running
|
||||
? <ResponseStopWatch startMillis={response.duration} />
|
||||
: <ResponseTime duration={response.duration} />}
|
||||
<ResponseSize size={responseSize} />
|
||||
</div>
|
||||
|
||||
<div className="separator" />
|
||||
<div className="flex items-center response-pane-actions">
|
||||
{focusedTab?.responsePaneTab === 'timeline' ? (
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
) : (item?.response && !item?.response?.error) ? (
|
||||
<ResponsePaneActions item={item} collection={collection} responseSize={responseSize} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className="flex flex-col min-h-0 relative px-4 pt-3 auto overflow-auto"
|
||||
className="flex flex-col min-h-0 relative px-4 auto overflow-auto"
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
height: hasScriptError && showScriptErrorCard ? 'auto' : '100%'
|
||||
|
||||
@@ -6,7 +6,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
@@ -20,7 +20,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.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;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user