mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c1a6ca71e | ||
|
|
d2227b2b05 | ||
|
|
dc03b6a761 | ||
|
|
e22f164cbc | ||
|
|
580d681e0a | ||
|
|
89b721d726 | ||
|
|
1110a4edda | ||
|
|
f69332d9c3 | ||
|
|
6947860204 | ||
|
|
963b0c257f | ||
|
|
33f8900705 | ||
|
|
22a14aa67a | ||
|
|
60c96f7d27 | ||
|
|
c8de57aa51 | ||
|
|
827c480689 | ||
|
|
1c869013c6 | ||
|
|
404a516fef | ||
|
|
e26075060e | ||
|
|
c524f40ab2 | ||
|
|
3e563ea126 | ||
|
|
a0cb53445f | ||
|
|
84bd603e11 | ||
|
|
c3236d4eb1 | ||
|
|
4a4208f272 | ||
|
|
d24f1a1054 | ||
|
|
86200a8f11 | ||
|
|
cf0ede1a83 | ||
|
|
342a39bcb4 | ||
|
|
e7d332c7d7 | ||
|
|
689d886e74 | ||
|
|
7a8e5198ff | ||
|
|
118ceacf46 | ||
|
|
a21615a5fb | ||
|
|
2ee2e270b0 | ||
|
|
9d6ba4691c | ||
|
|
104bd272f9 | ||
|
|
62a184c386 | ||
|
|
4663a1246c | ||
|
|
0efd782bcb | ||
|
|
a0903a5842 | ||
|
|
8202182074 | ||
|
|
ee4d4e3361 | ||
|
|
b76ddcd007 | ||
|
|
6f6dedbb9c | ||
|
|
37b1c043eb | ||
|
|
58bc247c53 | ||
|
|
c5b509115a | ||
|
|
524a59aed4 | ||
|
|
be49ef5f12 | ||
|
|
d4f05fa843 | ||
|
|
adedd08e8a | ||
|
|
7dd0d10a5d | ||
|
|
d9ef1692fe | ||
|
|
b88848f0dc | ||
|
|
abc26e5c5a | ||
|
|
d7733552bf | ||
|
|
6852cc6631 | ||
|
|
8bfb2591c2 | ||
|
|
05a290839b | ||
|
|
80f9e33be5 | ||
|
|
5a78dfa210 | ||
|
|
61caca59ee | ||
|
|
383c5ba782 | ||
|
|
28fbaa3470 | ||
|
|
27dcf78e73 | ||
|
|
25883b84fa | ||
|
|
667811cbd4 | ||
|
|
7839e93a57 | ||
|
|
11c60273b4 | ||
|
|
1dcff56c78 | ||
|
|
2e32423869 | ||
|
|
c328281f21 | ||
|
|
cc261326fc | ||
|
|
050ee2680f | ||
|
|
b2c28465e9 | ||
|
|
cd36335c60 | ||
|
|
d89f12c071 | ||
|
|
905f459ed0 | ||
|
|
b800055df4 | ||
|
|
b1d2b798ba | ||
|
|
4a403a253e | ||
|
|
a45628dd85 | ||
|
|
977637e556 | ||
|
|
3d63db806d | ||
|
|
1ec24d1138 | ||
|
|
fa40685a6a | ||
|
|
037013005f | ||
|
|
0c42298ce6 | ||
|
|
84ce75263b | ||
|
|
5c8d0a9e8a | ||
|
|
b70bbf78b1 | ||
|
|
43b9412ddb | ||
|
|
b56972fd93 | ||
|
|
c102ac527a | ||
|
|
45229b1af7 | ||
|
|
f9a3fb2f1b | ||
|
|
65d8a707d8 | ||
|
|
cc6bf45d5f | ||
|
|
8fbb777665 | ||
|
|
0e041d460c | ||
|
|
2e3b296021 | ||
|
|
405b50edcd | ||
|
|
fff540010e | ||
|
|
e513694912 | ||
|
|
d01cada16c | ||
|
|
dd4fecfd1c | ||
|
|
095d7c6bcb | ||
|
|
d165a04377 | ||
|
|
d3d1e47950 | ||
|
|
9c14941c15 | ||
|
|
1627f65bd7 | ||
|
|
19f4f3c1a5 | ||
|
|
ae70680ceb | ||
|
|
60fc13c765 | ||
|
|
60c3d41c8e | ||
|
|
fb8ff37d83 | ||
|
|
0d9b30e730 | ||
|
|
695f42df80 | ||
|
|
6b43159be2 | ||
|
|
21c9c8b4fb | ||
|
|
c4abe54c3f | ||
|
|
dd71c9e71b | ||
|
|
2be3e4bf69 | ||
|
|
f34e9f7b26 | ||
|
|
76b0729af3 | ||
|
|
4877bc3849 | ||
|
|
0742e3415c | ||
|
|
ae7e3a722c | ||
|
|
7f2e19250f | ||
|
|
4e16e954ef | ||
|
|
b6c3205474 | ||
|
|
23076b41c6 | ||
|
|
b5116b54af | ||
|
|
83aaa21b5b | ||
|
|
e1e7b37ce5 | ||
|
|
8dab9268f2 | ||
|
|
4eed999db1 | ||
|
|
c29ab50a3d | ||
|
|
5e1d6cba4a | ||
|
|
a645d1459c | ||
|
|
24e11a864c | ||
|
|
87a4778a91 | ||
|
|
0750af4c68 | ||
|
|
60e613fac8 | ||
|
|
b75baf57ba | ||
|
|
137df3c5c0 | ||
|
|
6ef2daebbd | ||
|
|
55f85e3728 | ||
|
|
f0269069d2 | ||
|
|
61dbca3243 | ||
|
|
f21cb240c4 | ||
|
|
ca46e14732 | ||
|
|
87f6000b85 | ||
|
|
36d0550472 | ||
|
|
ee4734c957 | ||
|
|
02f9fc0a7b | ||
|
|
6ce657d891 |
23
.github/workflows/unit-tests.yml
vendored
Normal file
23
.github/workflows/unit-tests.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Unit Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Install dependencies
|
||||
run: npm i --legacy-peer-deps
|
||||
- name: Test Package bruno-lang
|
||||
run: npm run test --workspace=packages/bruno-lang
|
||||
- name: Test Package bruno-schema
|
||||
run: npm run test --workspace=packages/bruno-schema
|
||||
- name: Test Package bruno-app
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
BIN
assets/images/landing-2.png
Normal file
BIN
assets/images/landing-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 537 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 436 KiB |
@@ -1,6 +1,16 @@
|
||||
## development
|
||||
|
||||
Bruno is deing developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
|
||||
|
||||
### Dependencies
|
||||
* NodeJS v18
|
||||
|
||||
###
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i
|
||||
|
||||
@@ -14,14 +24,17 @@ npm run dev --workspace=packages/bruno-electron
|
||||
npm run build --workspace=packages/bruno-app
|
||||
```
|
||||
|
||||
## fix
|
||||
### fix
|
||||
|
||||
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
|
||||
|
||||
# testing
|
||||
### testing
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
```
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
"workspaces": [
|
||||
"packages/bruno-app",
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-tauri",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
"packages/bruno-testbench",
|
||||
"packages/bruno-graphql-docs"
|
||||
],
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
module.exports = {
|
||||
reactStrictMode: true,
|
||||
reactStrictMode: false,
|
||||
publicRuntimeConfig: {
|
||||
CI: process.env.CI,
|
||||
PLAYWRIGHT: process.env.PLAYWRIGHT
|
||||
PLAYWRIGHT: process.env.PLAYWRIGHT,
|
||||
ENV: process.env.ENV
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
// Fixes npm packages that depend on `fs` module
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "@usebruno/app",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"dev": "cross-env ENV=dev next dev",
|
||||
"build": "next build && next export",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "jest",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -15,8 +17,8 @@
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@tabler/icons": "^1.46.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/schema": "0.2.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.2.0",
|
||||
"axios": "^0.26.0",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
@@ -26,7 +28,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"formik": "^2.2.9",
|
||||
"graphiql": "^1.5.9",
|
||||
"graphql": "^16.2.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
@@ -34,17 +36,19 @@
|
||||
"markdown-it": "^13.0.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "12.3.1",
|
||||
"next": "12.3.3",
|
||||
"path": "^0.12.7",
|
||||
"platform": "^1.3.6",
|
||||
"posthog-node": "^2.1.0",
|
||||
"qs": "^6.11.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "18.2.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-github-btn": "^1.4.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-tabs": "^3.2.3",
|
||||
"reckonjs": "^0.1.2",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"split-on-first": "^3.0.0",
|
||||
"styled-components": "^5.3.3",
|
||||
@@ -58,6 +62,7 @@
|
||||
"@babel/preset-react": "^7.16.0",
|
||||
"@babel/runtime": "^7.16.3",
|
||||
"babel-loader": "^8.2.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.5.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^3.0.1",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { get, post, put } from './base';
|
||||
|
||||
// not used. kept as a placeholder for reference while implementing license key stuff
|
||||
const AuthApi = {
|
||||
whoami: () => get('auth/v1/user/whoami'),
|
||||
signup: (params) => post('auth/v1/user/signup', params),
|
||||
login: (params) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window.require('electron');
|
||||
|
||||
ipcRenderer
|
||||
.invoke('bruno-account-request', {
|
||||
data: params,
|
||||
method: 'POST',
|
||||
url: `${process.env.NEXT_PUBLIC_BRUNO_SERVER_API}/auth/v1/user/login`
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default AuthApi;
|
||||
@@ -1,30 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_GRAFNODE_SERVER_API
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
return {
|
||||
...config,
|
||||
headers: headers
|
||||
};
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
return Promise.reject(error.response ? error.response.data : error);
|
||||
}
|
||||
);
|
||||
|
||||
const { get, post, put, delete: destroy } = apiClient;
|
||||
|
||||
export { get, post, put, destroy };
|
||||
@@ -6,12 +6,22 @@ const StyledWrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -27,6 +37,9 @@ const StyledWrapper = styled.div`
|
||||
.cm-s-monokai span.cm-atom{
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
|
||||
.cm-variable-valid{color: green}
|
||||
.cm-variable-invalid{color: red}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getEnvironmentVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
let CodeMirror;
|
||||
@@ -15,7 +18,7 @@ if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
|
||||
export default class QueryEditor extends React.Component {
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -23,6 +26,7 @@ export default class QueryEditor extends React.Component {
|
||||
// editor is updated, which can later be used to protect the editor from
|
||||
// unnecessary updates during the update lifecycle.
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -39,6 +43,7 @@ export default class QueryEditor extends React.Component {
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
readOnly: this.props.readOnly,
|
||||
scrollbarStyle: "overlay",
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
extraKeys: {
|
||||
'Cmd-Enter': () => {
|
||||
@@ -70,6 +75,7 @@ export default class QueryEditor extends React.Component {
|
||||
}));
|
||||
if (editor) {
|
||||
editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +94,13 @@ export default class QueryEditor extends React.Component {
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
this.editor.setOption('mode', this.props.mode);
|
||||
}
|
||||
|
||||
if(this.editor) {
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.addOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
@@ -116,6 +128,15 @@ export default class QueryEditor extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
addOverlay = () => {
|
||||
const mode = this.props.mode || 'application/ld+json';
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
}
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.cachedValue = this.editor.getValue();
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-enviroment {
|
||||
background-color: ${(props) => props.theme.sidebar.workspace.bg};
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 15px;
|
||||
|
||||
.caret {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useRef, forwardRef, useState } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconSettings, IconCaretDown, IconDatabase } from '@tabler/icons';
|
||||
import { IconSettings, IconCaretDown, IconDatabase, IconDatabaseOff } from '@tabler/icons';
|
||||
import EnvironmentSettings from '../EnvironmentSettings';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -63,13 +63,14 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
onSelect(null);
|
||||
}}
|
||||
>
|
||||
<span>No Environment</span>
|
||||
<IconDatabaseOff size={18} strokeWidth={1.5} />
|
||||
<span className='ml-2'>No Environment</span>
|
||||
</div>
|
||||
<div className="dropdown-item border-top" onClick={() => setOpenSettingsModal(true)}>
|
||||
<div className="pr-2 text-gray-600">
|
||||
<IconSettings size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span>Settings</span>
|
||||
<span>Configure</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@ import DeleteEnvironment from '../../DeleteEnvironment';
|
||||
const EnvironmentDetails = ({ environment, collection }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
console.log(environment);
|
||||
|
||||
return (
|
||||
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState, forwardRef, useRef } from 'react';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -9,8 +10,13 @@ const EnvironmentList = ({ collection }) => {
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}, [environments]);
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
if(environment) {
|
||||
setSelectedEnvironment(environment);
|
||||
} else {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
}
|
||||
}, [collection, environments]);
|
||||
|
||||
if (!selectedEnvironment) {
|
||||
return null;
|
||||
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +39,7 @@ const Wrapper = styled.div`
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
background: transparent;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -3,11 +3,15 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addFormUrlEncodedParam, updateFormUrlEncodedParam, deleteFormUrlEncodedParam } from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.body.formUrlEncoded') : get(item, 'request.body.formUrlEncoded');
|
||||
|
||||
const addParam = () => {
|
||||
@@ -19,6 +23,8 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
switch (type) {
|
||||
@@ -30,10 +36,6 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
param.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -65,7 +67,6 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -87,27 +88,17 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, param, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -6,10 +6,14 @@ import { IconRefresh, IconLoader2, IconBook, IconDownload } from '@tabler/icons'
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryEditor from 'components/RequestPane/QueryEditor';
|
||||
import GraphQLVariables from 'components/RequestPane/GraphQLVariables';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
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 { findEnvironmentInCollection } from 'utils/collections';
|
||||
import useGraphqlSchema from './useGraphqlSchema';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -18,17 +22,20 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
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 url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
|
||||
let {
|
||||
schema,
|
||||
loadSchema,
|
||||
isLoading: isSchemaLoading,
|
||||
error: schemaError
|
||||
} = useGraphqlSchema(url);
|
||||
} = useGraphqlSchema(url, environment);
|
||||
|
||||
const loadGqlSchema = () => {
|
||||
if(!isSchemaLoading) {
|
||||
@@ -67,6 +74,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
switch (tab) {
|
||||
case 'query': {
|
||||
return <QueryEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
schema={schema}
|
||||
width={leftPaneWidth}
|
||||
@@ -77,9 +85,18 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
onClickReference={handleGqlClickReference}
|
||||
/>;
|
||||
}
|
||||
case 'variables': {
|
||||
return <GraphQLVariables item={item} variables={variables} collection={collection} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <Tests item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -107,9 +124,18 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
<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('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center" style={{fontSize: 13}}>
|
||||
<div className='flex items-center cursor-pointer hover:underline' onClick={loadGqlSchema}>
|
||||
{isSchemaLoading ? (
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
|
||||
import { buildClientSchema } from 'graphql';
|
||||
import { fetchGqlSchema } from 'utils/network';
|
||||
import { simpleHash } from 'utils/common';
|
||||
|
||||
const schemaHashPrefix = 'bruno.graphqlSchema';
|
||||
|
||||
const fetchSchema = (endpoint) => {
|
||||
const introspectionQuery = getIntrospectionQuery();
|
||||
const queryParams = {
|
||||
query: introspectionQuery
|
||||
};
|
||||
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(queryParams)
|
||||
});
|
||||
}
|
||||
|
||||
const useGraphqlSchema = (endpoint) => {
|
||||
const useGraphqlSchema = (endpoint, environment) => {
|
||||
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -40,8 +25,8 @@ const useGraphqlSchema = (endpoint) => {
|
||||
|
||||
const loadSchema = () => {
|
||||
setIsLoading(true);
|
||||
fetchSchema(endpoint)
|
||||
.then((res) => res.json())
|
||||
fetchGqlSchema(endpoint, environment)
|
||||
.then((res) => res.data)
|
||||
.then((s) => {
|
||||
if (s && s.data) {
|
||||
setSchema(buildClientSchema(s.data));
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlVariables({
|
||||
variables: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection} value={variables || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GraphQLVariables;
|
||||
@@ -7,6 +7,8 @@ import QueryParams from 'components/RequestPane/QueryParams';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import RequestBody from 'components/RequestPane/RequestBody';
|
||||
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
@@ -34,6 +36,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script item={item} collection={collection} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <Tests item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -67,6 +75,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
@@ -75,7 +89,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
<section className={`flex w-full ${focusedTab.requestPaneTab === 'script' ? '' : 'mt-5'}`}>{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +39,8 @@ const Wrapper = styled.div`
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
color: ${(props) => props.theme.table.input.color};
|
||||
background: transparent;
|
||||
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
|
||||
@@ -3,11 +3,15 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam } from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const MultipartFormParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.body.multipartForm') : get(item, 'request.body.multipartForm');
|
||||
|
||||
const addParam = () => {
|
||||
@@ -19,6 +23,8 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
switch (type) {
|
||||
@@ -30,10 +36,6 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
param.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -65,7 +67,6 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -87,27 +88,17 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||
<SingleLineEditor
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
value={param.value}
|
||||
onChange={(newValue) => handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, param, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -14,6 +14,16 @@ const StyledWrapper = styled.div`
|
||||
|
||||
// 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-property, .cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
@@ -29,6 +39,9 @@ const StyledWrapper = styled.div`
|
||||
.cm-s-monokai span.cm-atom{
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
|
||||
.cm-variable-valid{color: green}
|
||||
.cm-variable-invalid{color: red}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import MD from 'markdown-it';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
import onHasCompletion from './onHasCompletion';
|
||||
@@ -29,6 +32,7 @@ export default class QueryEditor extends React.Component {
|
||||
// editor is updated, which can later be used to protect the editor from
|
||||
// unnecessary updates during the update lifecycle.
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -37,12 +41,17 @@ export default class QueryEditor extends React.Component {
|
||||
lineNumbers: true,
|
||||
tabSize: 2,
|
||||
mode: 'graphql',
|
||||
// mode: 'brunovariables',
|
||||
brunoVarInfo: {
|
||||
variables: getAllVariables(this.props.collection),
|
||||
},
|
||||
theme: this.props.editorTheme || 'graphiql',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
showCursorWhenSelecting: true,
|
||||
scrollbarStyle: "overlay",
|
||||
readOnly: this.props.readOnly ? 'nocursor' : false,
|
||||
foldGutter: {
|
||||
minFoldSize: 4
|
||||
@@ -129,6 +138,7 @@ export default class QueryEditor extends React.Component {
|
||||
editor.on('hasCompletion', this._onHasCompletion);
|
||||
editor.on('beforeChange', this._onBeforeChange);
|
||||
}
|
||||
this.addOverlay();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -151,6 +161,11 @@ export default class QueryEditor extends React.Component {
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
let variables = getAllVariables(this.props.collection);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.editor.options.brunoVarInfo.variables = variables;
|
||||
this.addOverlay();
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
@@ -163,6 +178,16 @@ export default class QueryEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: Overlay is messing up with schema hint
|
||||
// Fix this
|
||||
addOverlay = () => {
|
||||
// let variables = getAllVariables(this.props.collection);
|
||||
// this.variables = variables;
|
||||
|
||||
// defineCodeMirrorBrunoVariablesMode(variables, 'graphql');
|
||||
// this.editor.setOption('mode', 'brunovariables');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledWrapper
|
||||
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,16 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addQueryParam, updateQueryParam, deleteQueryParam } from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
|
||||
|
||||
const handleAddParam = () => {
|
||||
@@ -20,6 +24,8 @@ const QueryParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
|
||||
@@ -32,10 +38,6 @@ const QueryParams = ({ item, collection }) => {
|
||||
param.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
param.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -68,7 +70,6 @@ const QueryParams = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -90,27 +91,17 @@ const QueryParams = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'description')}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleParamChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, param, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect} from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import HttpMethodSelector from './HttpMethodSelector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SendIcon from 'components/Icons/Send';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const { theme } = useTheme();
|
||||
const { theme, storedTheme } = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
|
||||
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
|
||||
|
||||
useEffect(() => {
|
||||
const el = document.querySelector(".method-selector-container");
|
||||
setMethodSelectorWidth(el.offsetWidth);
|
||||
}, [method]);
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const onUrlChange = (value) => {
|
||||
dispatch(
|
||||
requestUrlChanged({
|
||||
@@ -38,16 +48,21 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<div className="flex items-center h-full method-selector-container">
|
||||
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
|
||||
</div>
|
||||
<div className="flex items-center flex-grow input-container h-full">
|
||||
<input
|
||||
className="px-3 w-full mousetrap"
|
||||
type="text"
|
||||
value={url}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={(event) => onUrlChange(event.target.value)}
|
||||
<div
|
||||
className="flex items-center flex-grow input-container h-full"
|
||||
style={{
|
||||
color: 'yellow',
|
||||
width: `calc(100% - ${methodSelectorWidth}px)`,
|
||||
maxWidth: `calc(100% - ${methodSelectorWidth}px)`
|
||||
}}
|
||||
>
|
||||
<SingleLineEditor
|
||||
value={url}
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
onChange={(newValue) => onUrlChange(newValue)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
<SendIcon color={theme.requestTabPanel.url.icon} width={22}/>
|
||||
|
||||
@@ -45,7 +45,7 @@ const RequestBody = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor theme={storedTheme} value={bodyContent[bodyMode] || ''} onEdit={onEdit} onRun={onRun} onSave={onSave} mode={codeMirrorMode[bodyMode]} />
|
||||
<CodeEditor collection={collection} theme={storedTheme} value={bodyContent[bodyMode] || ''} onEdit={onEdit} onRun={onRun} onSave={onSave} mode={codeMirrorMode[bodyMode]} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
@@ -18,6 +19,14 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,15 @@ import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
|
||||
const addHeader = () => {
|
||||
@@ -19,6 +23,8 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleHeaderValueChange = (e, _header, type) => {
|
||||
const header = cloneDeep(_header);
|
||||
switch (type) {
|
||||
@@ -30,10 +36,6 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
header.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'description': {
|
||||
header.description = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
header.enabled = e.target.checked;
|
||||
break;
|
||||
@@ -65,7 +67,6 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
<tr>
|
||||
<td>Key</td>
|
||||
<td>Value</td>
|
||||
<td>Description</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -87,27 +88,17 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
<SingleLineEditor
|
||||
value={header.value}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'value')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={header.description}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'description')}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleHeaderValueChange({
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
}, header, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: rgb(155 155 155);
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Script = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestScript = item.draft ? get(item, 'draft.request.script.req') : get(item, 'request.script.req');
|
||||
const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onResponseScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateResponseScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className='flex-1'>
|
||||
<div className='mb-1 title'>Request</div>
|
||||
<CodeEditor
|
||||
collection={collection} value={requestScript || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex-1 mt-6'>
|
||||
<div className='mt-1 mb-1 title'>Response</div>
|
||||
<CodeEditor
|
||||
collection={collection} value={responseScript || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Script;
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 220px);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
45
packages/bruno-app/src/components/RequestPane/Tests/index.js
Normal file
45
packages/bruno-app/src/components/RequestPane/Tests/index.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Tests = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tests = item.draft ? get(item, 'draft.request.tests') : get(item, 'request.tests');
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestTests({
|
||||
tests: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<CodeEditor
|
||||
collection={collection} value={tests || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onEdit}
|
||||
mode='javascript'
|
||||
onRun={onRun}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tests;
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const RequestNotFound = ({ itemUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||
|
||||
const closeTab = () => {
|
||||
dispatch(
|
||||
@@ -13,11 +14,25 @@ const RequestNotFound = ({ itemUid }) => {
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setShowErrorMessage(true);
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
// add a delay component in react that shows a loading spinner
|
||||
// and then shows the error message after a delay
|
||||
// this will prevent the error message from flashing on the screen
|
||||
|
||||
if(!showErrorMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 px-6">
|
||||
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700 bg-yellow-100 p-4">
|
||||
<div>Request no longer exists.</div>
|
||||
<div className="mt-2">This can happen when the yml file associated with this request was deleted on your filesystem.</div>
|
||||
<div className="mt-2">This can happen when the .bru file associated with this request was deleted on your filesystem.</div>
|
||||
</div>
|
||||
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
|
||||
Close Tab
|
||||
|
||||
@@ -12,6 +12,7 @@ import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import RequestNotFound from './RequestNotFound';
|
||||
import QueryUrl from 'components/RequestPane/QueryUrl';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError';
|
||||
import RunnerResults from 'components/RunnerResults';
|
||||
import { DocExplorer } from '@usebruno/graphql-docs';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -112,6 +113,11 @@ const RequestTabPanel = () => {
|
||||
return <div className="pb-4 px-4">Collection not found!</div>;
|
||||
}
|
||||
|
||||
const showRunner = collection.showRunner;
|
||||
if(showRunner) {
|
||||
return <RunnerResults collection={collection}/>;
|
||||
}
|
||||
|
||||
const item = findItemInCollection(collection, activeTabUid);
|
||||
if (!item || !item.uid) {
|
||||
return <RequestNotFound itemUid={activeTabUid} />;
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import React from 'react';
|
||||
import { IconFiles } from '@tabler/icons';
|
||||
import { IconFiles, IconRun } from '@tabler/icons';
|
||||
import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
|
||||
import VariablesView from 'components/VariablesView';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { toggleRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionToolBar = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleRun = () => {
|
||||
dispatch(toggleRunnerView({
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center p-2">
|
||||
@@ -12,6 +23,10 @@ const CollectionToolBar = ({ collection }) => {
|
||||
<span className="ml-2 mr-4 font-semibold">{collection.name}</span>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end">
|
||||
<span className="mr-2">
|
||||
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
|
||||
</span>
|
||||
<VariablesView collection={collection}/>
|
||||
<EnvironmentSelector collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
|
||||
const RequestTabNotFound = ({handleCloseClick}) => {
|
||||
const [showErrorMessage, setShowErrorMessage] = useState(false);
|
||||
|
||||
// add a delay component in react that shows a loading spinner
|
||||
// and then shows the error message after a delay
|
||||
// this will prevent the error message from flashing on the screen
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setShowErrorMessage(true);
|
||||
}, 300);
|
||||
}, []);
|
||||
|
||||
if(!showErrorMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center tab-label pl-2">
|
||||
{showErrorMessage ? (
|
||||
<>
|
||||
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Not Found</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestTabNotFound;
|
||||
@@ -4,7 +4,7 @@ import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
|
||||
const RequestTab = ({ tab, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -61,18 +61,7 @@ const RequestTab = ({ tab, collection }) => {
|
||||
if (!item) {
|
||||
return (
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
<div className="flex items-center tab-label pl-2">
|
||||
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<span className="ml-1">Not Found</span>
|
||||
</div>
|
||||
<div className="flex px-2 close-icon-container" onClick={(e) => handleCloseClick(e)}>
|
||||
<svg focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" className="close-icon">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<RequestTabNotFound handleCloseClick={handleCloseClick} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
import { IconHome2, IconChevronRight, IconChevronLeft } from '@tabler/icons';
|
||||
import { IconChevronRight, IconChevronLeft } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
@@ -76,6 +76,8 @@ const RequestTabs = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const showRunner = activeCollection && activeCollection.showRunner;
|
||||
|
||||
// Todo: Must support ephermal requests
|
||||
return (
|
||||
<StyledWrapper className={getRootClassname()}>
|
||||
@@ -83,59 +85,61 @@ const RequestTabs = () => {
|
||||
{collectionRequestTabs && collectionRequestTabs.length ? (
|
||||
<>
|
||||
<CollectionToolBar collection={activeCollection} />
|
||||
<div className="flex items-center pl-4">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronLeft size={18} strokeWidth={1.5} />
|
||||
{!showRunner ? (
|
||||
<div className="flex items-center pl-4">
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={leftSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronLeft size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
|
||||
<div className="flex items-center home-icon-container">
|
||||
<IconHome2 size={18} strokeWidth={1.5}/>
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
|
||||
<div className="flex items-center home-icon-container">
|
||||
<IconHome2 size={18} strokeWidth={1.5}/>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<li key={tab.uid} className={getTabClassname(tab, index)} role="tab" onClick={() => handleClick(tab)}>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
</li> */}
|
||||
</ul>
|
||||
<ul role="tablist" style={{ maxWidth: maxTablistWidth }} ref={tabsRef}>
|
||||
{collectionRequestTabs && collectionRequestTabs.length
|
||||
? collectionRequestTabs.map((tab, index) => {
|
||||
return (
|
||||
<li key={tab.uid} className={getTabClassname(tab, index)} role="tab" onClick={() => handleClick(tab)}>
|
||||
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} activeTab={activeTab} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</ul>
|
||||
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={rightSlide}>
|
||||
<ul role="tablist">
|
||||
{showChevrons ? (
|
||||
<li className="select-none short-tab" onClick={rightSlide}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronRight size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
</li>
|
||||
) : null}
|
||||
<li className="select-none short-tab" id="create-new-tab" onClick={createNewTab}>
|
||||
<div className="flex items-center">
|
||||
<IconChevronRight size={18} strokeWidth={1.5} />
|
||||
<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>
|
||||
) : null}
|
||||
<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">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab choose-request">
|
||||
<div className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</li> */}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -7,9 +7,6 @@ const NetworkError = ({ onClose }) => {
|
||||
<div className="flex items-start">
|
||||
<div className="ml-3 flex-1">
|
||||
<p className="text-sm font-medium text-red-800">Network Error</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Please note that if you are using Bruno on the web, then the api you are connecting to must allow CORS. If not, please use the chrome extension or the desktop app
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import React from 'react';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryResult = ({ value, width }) => {
|
||||
const QueryResult = ({ item, collection, value, width, disableRunEventListener }) => {
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onRun = () => {
|
||||
if(disableRunEventListener) {
|
||||
return;
|
||||
}
|
||||
dispatch(sendRequest(item, collection.uid));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-3 w-full" style={{ maxWidth: width }}>
|
||||
<div className="h-full">
|
||||
<CodeEditor theme={storedTheme} value={value || ''} readOnly />
|
||||
<CodeEditor collection={collection} theme={storedTheme} onRun={onRun} value={value || ''} readOnly />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -25,6 +25,14 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.some-tests-failed {
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
}
|
||||
|
||||
.all-tests-passed {
|
||||
color: ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.test-success {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
.test-failure {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TestResults = ({ results }) => {
|
||||
if (!results || !results.length) {
|
||||
return (
|
||||
<div className="px-3">
|
||||
No tests found
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const passedTests = results.filter((result) => result.status === 'pass');
|
||||
const failedTests = results.filter((result) => result.status === 'fail');
|
||||
|
||||
return (
|
||||
<StyledWrapper className='flex flex-col px-3'>
|
||||
<div className="py-2 font-medium test-summary">
|
||||
Tests ({results.length}/{results.length}), Passed: {passedTests.length}, Failed: {failedTests.length}
|
||||
</div>
|
||||
<ul className="">
|
||||
{results.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success">
|
||||
✔ {result.description}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure">
|
||||
✘ {result.description}
|
||||
</span>
|
||||
<br />
|
||||
<span className="error-message pl-8">
|
||||
{result.error}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestResults;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
const TestResultsLabel = ({ results }) => {
|
||||
if(!results || !results.length) {
|
||||
return 'Tests';
|
||||
}
|
||||
|
||||
const numberOfTests = results.length;
|
||||
const numberOfFailedTests = results.filter(result => result.status === 'fail').length;
|
||||
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<div>Tests</div>
|
||||
{numberOfFailedTests ? (
|
||||
<sup className='sups some-tests-failed ml-1 font-medium'>
|
||||
{numberOfFailedTests}
|
||||
</sup>
|
||||
) : (
|
||||
<sup className='sups all-tests-passed ml-1 font-medium'>
|
||||
{numberOfTests}
|
||||
</sup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestResultsLabel;
|
||||
@@ -0,0 +1,24 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.line {
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
font-family: Inter, sans-serif !important;
|
||||
|
||||
.arrow {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.request {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
&.response {
|
||||
color: ${(props) => props.theme.colors.text.purple};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Timeline = ({ request, response}) => {
|
||||
const requestHeaders = [];
|
||||
const responseHeaders = response.headers || [];
|
||||
|
||||
request = request || {};
|
||||
response = response || {};
|
||||
|
||||
forOwn(request.headers, (value, key) => {
|
||||
requestHeaders.push({
|
||||
name: key,
|
||||
value
|
||||
});
|
||||
});
|
||||
|
||||
let requestData = safeStringifyJSON(request.data);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-3 pb-4 w-full">
|
||||
<div>
|
||||
<pre className='line request font-bold'>
|
||||
<span className="arrow">{'>'}</span> {request.method} {request.url}
|
||||
</pre>
|
||||
{requestHeaders.map((h) => {
|
||||
return (
|
||||
<pre className='line request' key={h.name}>
|
||||
<span className="arrow">{'>'}</span> {h.name}: {h.value}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
|
||||
{requestData ? (
|
||||
<pre className='line request'>
|
||||
<span className="arrow">{'>'}</span> data {requestData}
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className='mt-4'>
|
||||
<pre className='line response font-bold'>
|
||||
<span className="arrow">{'<'}</span> {response.status} {response.statusText}
|
||||
</pre>
|
||||
|
||||
{responseHeaders.map((h) => {
|
||||
return (
|
||||
<pre className='line response' key={h[0]}>
|
||||
<span className="arrow">{'<'}</span> {h[0]}: {h[1]}
|
||||
</pre>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Timeline;
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import find from 'lodash/find';
|
||||
import classnames from 'classnames';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryResult from './QueryResult';
|
||||
@@ -10,13 +11,16 @@ import ResponseHeaders from './ResponseHeaders';
|
||||
import StatusCode from './StatusCode';
|
||||
import ResponseTime from './ResponseTime';
|
||||
import ResponseSize from './ResponseSize';
|
||||
import Timeline from './Timeline';
|
||||
import TestResults from './TestResults';
|
||||
import TestResultsLabel from './TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isLoading = item.response && item.response.state === 'sending';
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -32,11 +36,22 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
return <QueryResult width={rightPaneWidth} value={response.data ? JSON.stringify(response.data, null, 2) : ''} />;
|
||||
return <QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
value={response.data ? safeStringifyJSON(response.data, true) : ''}
|
||||
/>;
|
||||
}
|
||||
case 'headers': {
|
||||
return <ResponseHeaders headers={response.headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline request={item.requestSent} response={item.response}/>;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={item.testResults} />;
|
||||
}
|
||||
|
||||
default: {
|
||||
return <div>404 | Not found</div>;
|
||||
@@ -84,6 +99,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={item.testResults} />
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<StatusCode status={response.status} />
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.some-tests-failed {
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
}
|
||||
|
||||
.all-tests-passed {
|
||||
color: ${(props) => props.theme.colors.text.green} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import classnames from 'classnames';
|
||||
import { safeStringifyJSON } from 'utils/common';
|
||||
import QueryResult from 'components/ResponsePane/QueryResult';
|
||||
import ResponseHeaders from 'components/ResponsePane/ResponseHeaders';
|
||||
import StatusCode from 'components/ResponsePane/StatusCode';
|
||||
import ResponseTime from 'components/ResponsePane/ResponseTime';
|
||||
import ResponseSize from 'components/ResponsePane/ResponseSize';
|
||||
import Timeline from 'components/ResponsePane/Timeline';
|
||||
import TestResults from 'components/ResponsePane/TestResults';
|
||||
import TestResultsLabel from 'components/ResponsePane/TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const [selectedTab, setSelectedTab] = useState('response');
|
||||
|
||||
const {
|
||||
requestSent,
|
||||
responseReceived,
|
||||
testResults
|
||||
} = item;
|
||||
|
||||
const headers = get(item, 'responseReceived.headers', {});
|
||||
const status = get(item, 'responseReceived.status', 0);
|
||||
const size = get(item, 'responseReceived.size', 0);
|
||||
const duration = get(item, 'responseReceived.duration', 0);
|
||||
|
||||
const selectTab = (tab) => setSelectedTab(tab);
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
return <QueryResult
|
||||
item={item}
|
||||
collection={collection}
|
||||
width={rightPaneWidth}
|
||||
disableRunEventListener={true}
|
||||
value={(responseReceived && responseReceived.data) ? safeStringifyJSON(responseReceived.data, true) : ''}
|
||||
/>;
|
||||
}
|
||||
case 'headers': {
|
||||
return <ResponseHeaders headers={headers} />;
|
||||
}
|
||||
case 'timeline': {
|
||||
return <Timeline request={requestSent} response={responseReceived} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={testResults} />;
|
||||
}
|
||||
|
||||
default: {
|
||||
return <div>404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === selectedTab
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex items-center px-3 tabs" role="tablist">
|
||||
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
|
||||
Response
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={testResults} />
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<StatusCode status={status} />
|
||||
<ResponseTime duration={duration} />
|
||||
<ResponseSize size={size} />
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex flex-grow mt-5">{getTabPanel(selectedTab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponsePane;
|
||||
@@ -0,0 +1,31 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.item-path {
|
||||
.link {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
.danger {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.test-summary {
|
||||
color: ${(props) => props.theme.tabs.active.border};
|
||||
}
|
||||
|
||||
/* test results */
|
||||
.test-success {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
|
||||
.test-failure {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
140
packages/bruno-app/src/components/RunnerResults/index.js
Normal file
140
packages/bruno-app/src/components/RunnerResults/index.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import path from 'path';
|
||||
import { get, each, cloneDeep } from 'lodash';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const getRelativePath = (fullPath, pathname) => {
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
const { dir, name } = path.parse(relativePath);
|
||||
return path.join(dir, name);
|
||||
}
|
||||
|
||||
export default function RunnerResults({collection}) {
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if(!collection.runnerResult) {
|
||||
setSelectedItem(null);
|
||||
}
|
||||
}, [collection, setSelectedItem]);
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const items = cloneDeep(get(collection, 'runnerResult.items', []));
|
||||
each(items, (item) => {
|
||||
const info = findItemInCollection(collectionCopy, item.uid);
|
||||
|
||||
item.name = info.name;
|
||||
item.type = info.type;
|
||||
item.filename = info.filename;
|
||||
item.pathname = info.pathname;
|
||||
item.relativePath = getRelativePath(collection.pathname, info.pathname);
|
||||
|
||||
if(item.status !== "error") {
|
||||
if(item.testResults) {
|
||||
const failed = item.testResults.filter((result) => result.status === 'fail');
|
||||
|
||||
item.testStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
item.testStatus = 'pass';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const passedRequests = items.filter((item) => item.status !== "error" && item.testStatus === 'pass');
|
||||
const failedRequests = items.filter((item) => item.status !== "error" && item.testStatus === 'fail');
|
||||
|
||||
return (
|
||||
<StyledWrapper className='px-4'>
|
||||
<div className='font-medium mt-6 mb-4 title flex items-center'>
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className='ml-2'/>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='flex flex-col flex-1'>
|
||||
<div className="py-2 font-medium test-summary">
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
|
||||
</div>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{item.status !== "error" && item.testStatus === 'pass' ? (
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5}/>
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5}/>
|
||||
)}
|
||||
</span>
|
||||
<span className={`mr-1 ml-2 ${(item.status == "error" || item.testStatus == 'fail') ? 'danger' : ''}`}>{item.relativePath}</span>
|
||||
{(item.status !== "error" && item.status !== "completed") ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5}/>
|
||||
) : (
|
||||
<span className='text-xs link cursor-pointer' onClick={() => setSelectedItem(item)}>
|
||||
(<span className='mr-1'>
|
||||
{get(item.responseReceived, 'status')}
|
||||
</span>
|
||||
<span>
|
||||
{get(item.responseReceived, 'statusText')}
|
||||
</span>)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{item.status == "error" ? (
|
||||
<div className="error-message pl-8 pt-2 text-xs">
|
||||
{item.error}
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.testResults ? item.testResults.map((result) => (
|
||||
<li key={result.uid} className="py-1">
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2"/>
|
||||
{result.description}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2"/>
|
||||
{result.description}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">
|
||||
{result.error}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
)): null}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className='flex flex-1' style={{width: '50%'}}>
|
||||
{selectedItem ? (
|
||||
<div className='flex flex-col w-full overflow-auto'>
|
||||
<div className="flex items-center px-3 mb-4 font-medium">
|
||||
<span className='mr-2'>{selectedItem.relativePath}</span>
|
||||
<span>
|
||||
{selectedItem.testStatus === 'pass' ? (
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5}/>
|
||||
) : (
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5}/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{/* <div className='px-3 mb-4 font-medium'>{selectedItem.relativePath}</div> */}
|
||||
<ResponsePane item={selectedItem} collection={collection}/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.bruno-modal-content {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { showRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import { flattenItems } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RunCollectionItem = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onSubmit = (recursive) => {
|
||||
dispatch(showRunnerView({
|
||||
collectionUid: collection.uid,
|
||||
}));
|
||||
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const runLength = item ? get(item, 'items.length', 0) : get(collection, 'items.length', 0);
|
||||
const items = flattenItems(item ? item.items : collection.items);
|
||||
const requestItems = items.filter((item) => item.type !== 'folder');
|
||||
const recursiveRunLength = requestItems.length;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="md" title='Collection Runner' hideFooter={true} handleCancel={onClose}>
|
||||
<div className='mb-1'>
|
||||
<span className='font-medium'>Run</span>
|
||||
<span className='ml-1 text-xs'>({runLength} requests)</span>
|
||||
</div>
|
||||
<div className='mb-8'>
|
||||
This will only run the requests in this folder.
|
||||
</div>
|
||||
|
||||
<div className='mb-1'>
|
||||
<span className='font-medium'>Recursive Run</span>
|
||||
<span className='ml-1 text-xs'>({recursiveRunLength} requests)</span>
|
||||
</div>
|
||||
<div className='mb-8'>
|
||||
This will run all the requests in this folder and all its subfolders.
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end bruno-modal-footer">
|
||||
<span className='mr-3'>
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary mr-3" onClick={() => onSubmit(true)}>
|
||||
Recursive Run
|
||||
</button>
|
||||
</span>
|
||||
<span>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary" onClick={() => onSubmit(false)}>
|
||||
Run
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunCollectionItem;
|
||||
@@ -70,7 +70,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging .collection-item-name {
|
||||
&.is-sidebar-dragging .collection-item-name {
|
||||
cursor: inherit;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2,10 +2,12 @@ import React, { useState, useRef, forwardRef, useEffect } from 'react';
|
||||
import range from 'lodash/range';
|
||||
import filter from 'lodash/filter';
|
||||
import classnames from 'classnames';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { collectionFolderClicked, hideRunnerView } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
@@ -13,6 +15,7 @@ import RequestMethod from './RequestMethod';
|
||||
import RenameCollectionItem from './RenameCollectionItem';
|
||||
import CloneCollectionItem from './CloneCollectionItem';
|
||||
import DeleteCollectionItem from './DeleteCollectionItem';
|
||||
import RunCollectionItem from './RunCollectionItem';
|
||||
import { isItemARequest, isItemAFolder, itemIsOpenedInTabs } from 'utils/tabs';
|
||||
import { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
@@ -23,7 +26,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isDragging = useSelector((state) => state.app.isDragging);
|
||||
const isSidebarDragging = useSelector((state) => state.app.isDragging);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);
|
||||
@@ -31,8 +34,32 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);
|
||||
const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);
|
||||
const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);
|
||||
const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);
|
||||
const [itemIsCollapsed, setItemisCollapsed] = useState(item.collapsed);
|
||||
|
||||
const [{ isDragging }, drag] = useDrag({
|
||||
type: `COLLECTION_ITEM_${collection.uid}`,
|
||||
item: item,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging()
|
||||
})
|
||||
});
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: `COLLECTION_ITEM_${collection.uid}`,
|
||||
drop: (draggedItem) => {
|
||||
if (draggedItem.uid !== item.uid) {
|
||||
dispatch(moveItem(collection.uid, draggedItem.uid, item.uid));
|
||||
}
|
||||
},
|
||||
canDrop: (draggedItem) => {
|
||||
return draggedItem.uid !== item.uid;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver()
|
||||
})
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (searchText && searchText.length) {
|
||||
setItemisCollapsed(false);
|
||||
@@ -59,6 +86,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
});
|
||||
|
||||
const handleClick = (event) => {
|
||||
dispatch(hideRunnerView({
|
||||
collectionUid: collection.uid
|
||||
}));
|
||||
if (isItemARequest(item)) {
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
@@ -91,7 +121,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const isFolder = isItemAFolder(item);
|
||||
|
||||
const className = classnames('flex flex-col w-full', {
|
||||
'is-dragging': isDragging
|
||||
'is-sidebar-dragging': isSidebarDragging
|
||||
});
|
||||
|
||||
if (searchText && searchText.length) {
|
||||
@@ -106,8 +136,18 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const requestItems = filter(item.items, (i) => isItemARequest(i));
|
||||
const folderItems = filter(item.items, (i) => isItemAFolder(i));
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
};
|
||||
|
||||
// we need to sort folder items by name alphabetically
|
||||
const sortFolderItems = (items = []) => {
|
||||
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));
|
||||
|
||||
return (
|
||||
<StyledWrapper className={className}>
|
||||
@@ -116,7 +156,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
{deleteItemModalOpen && <DeleteCollectionItem item={item} collection={collection} onClose={() => setDeleteItemModalOpen(false)} />}
|
||||
{newRequestModalOpen && <NewRequest item={item} collection={collection} onClose={() => setNewRequestModalOpen(false)} />}
|
||||
{newFolderModalOpen && <NewFolder item={item} collection={collection} onClose={() => setNewFolderModalOpen(false)} />}
|
||||
<div className={itemRowClassName}>
|
||||
{runCollectionModalOpen && <RunCollectionItem collection={collection} item={item} onClose={() => setRunCollectionModalOpen(false)} />}
|
||||
<div className={itemRowClassName} ref={(node) => drag(drop(node))}>
|
||||
<div className="flex items-center h-full w-full">
|
||||
{indents && indents.length
|
||||
? indents.map((i) => {
|
||||
@@ -176,6 +217,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
>
|
||||
New Folder
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setRunCollectionModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
@@ -214,13 +264,13 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
|
||||
{!itemIsCollapsed ? (
|
||||
<div>
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
button.submit {
|
||||
color: white;
|
||||
background-color: var(--color-background-danger) !important;
|
||||
border: inherit !important;
|
||||
|
||||
&:hover {
|
||||
border: inherit !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { deleteCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DeleteCollection = ({ onClose, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const onConfirm = () => {
|
||||
dispatch(deleteCollection(collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection deleted');
|
||||
})
|
||||
.catch(() => toast.error('An error occured while deleting the collection'));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title="Delete Collection" confirmText="Delete" handleConfirm={onConfirm} handleCancel={onClose}>
|
||||
Are you sure you want to delete the collection <span className="font-semibold">{collection.name}</span> ?
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteCollection;
|
||||
@@ -2,13 +2,13 @@ import React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { removeLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { removeCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
const RemoveLocalCollection = ({ onClose, collection }) => {
|
||||
const RemoveCollection = ({ onClose, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onConfirm = () => {
|
||||
dispatch(removeLocalCollection(collection.uid))
|
||||
dispatch(removeCollection(collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection removed');
|
||||
onClose();
|
||||
@@ -23,4 +23,4 @@ const RemoveLocalCollection = ({ onClose, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveLocalCollection;
|
||||
export default RemoveCollection;
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { recursivelyGetAllItemUids } from 'utils/collections';
|
||||
import { removeCollectionFromWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { removeLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
|
||||
const RemoveCollectionFromWorkspace = ({ onClose, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
|
||||
const onConfirm = () => {
|
||||
dispatch(removeCollectionFromWorkspace(activeWorkspaceUid, collection.uid))
|
||||
.then(() => {
|
||||
dispatch(
|
||||
closeTabs({
|
||||
tabUids: recursivelyGetAllItemUids(collection.items)
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => toast.success('Collection removed from workspace'))
|
||||
.catch((err) => console.log(err) && toast.error('An error occured while removing the collection'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Remove Collection from Workspace" confirmText="Remove" handleConfirm={onConfirm} handleCancel={onClose}>
|
||||
Are you sure you want to remove the collection <span className="font-semibold">{collection.name}</span> from this workspace?
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveCollectionFromWorkspace;
|
||||
@@ -52,6 +52,12 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#sidebar-collection-name {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -2,30 +2,30 @@ import React, { useState, forwardRef, useRef, useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import filter from 'lodash/filter';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { collectionClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
import CollectionItem from './CollectionItem';
|
||||
import RemoveCollectionFromWorkspace from './RemoveCollectionFromWorkspace';
|
||||
import RemoveLocalCollection from './RemoveLocalCollection';
|
||||
import RemoveCollection from './RemoveCollection';
|
||||
import RunCollectionItem from './CollectionItem/RunCollectionItem';
|
||||
import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';
|
||||
import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb, isLocalCollection } from 'utils/collections';
|
||||
import { isItemAFolder, isItemARequest, transformCollectionToSaveToIdb } from 'utils/collections';
|
||||
import exportCollection from 'utils/collections/export';
|
||||
|
||||
import RenameCollection from './RenameCollection';
|
||||
import DeleteCollection from './DeleteCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Collection = ({ collection, searchText }) => {
|
||||
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
|
||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
|
||||
const [showRemoveCollectionFromWSModal, setShowRemoveCollectionFromWSModal] = useState(false);
|
||||
const [showRemoveLocalCollectionModal, setShowRemoveLocalCollectionModal] = useState(false);
|
||||
const [showDeleteCollectionModal, setShowDeleteCollectionModal] = useState(false);
|
||||
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
|
||||
const [showRunCollectionModal, setShowRunCollectionModal] = useState(false);
|
||||
const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -61,27 +61,48 @@ const Collection = ({ collection, searchText }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const requestItems = filter(collection.items, (i) => isItemARequest(i));
|
||||
const folderItems = filter(collection.items, (i) => isItemAFolder(i));
|
||||
|
||||
const handleExportClick = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportCollection(transformCollectionToSaveToIdb(collectionCopy));
|
||||
};
|
||||
|
||||
const isLocal = isLocalCollection(collection);
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: `COLLECTION_ITEM_${collection.uid}`,
|
||||
drop: (draggedItem) => {
|
||||
dispatch(moveItemToRootOfCollection(collection.uid, draggedItem.uid));
|
||||
},
|
||||
canDrop: (draggedItem) => {
|
||||
// todo need to make sure that draggedItem belongs to the collection
|
||||
return true;
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver()
|
||||
})
|
||||
});
|
||||
|
||||
// we need to sort request items by seq property
|
||||
const sortRequestItems = (items = []) => {
|
||||
return items.sort((a, b) => a.seq - b.seq);
|
||||
};
|
||||
|
||||
// we need to sort folder items by name alphabetically
|
||||
const sortFolderItems = (items = []) => {
|
||||
return items.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
const requestItems = sortRequestItems(filter(collection.items, (i) => isItemARequest(i)));
|
||||
const folderItems = sortFolderItems(filter(collection.items, (i) => isItemAFolder(i)));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col">
|
||||
{showNewRequestModal && <NewRequest collection={collection} onClose={() => setShowNewRequestModal(false)} />}
|
||||
{showNewFolderModal && <NewFolder collection={collection} onClose={() => setShowNewFolderModal(false)} />}
|
||||
{showRenameCollectionModal && <RenameCollection collection={collection} onClose={() => setShowRenameCollectionModal(false)} />}
|
||||
{showRemoveCollectionFromWSModal && <RemoveCollectionFromWorkspace collection={collection} onClose={() => setShowRemoveCollectionFromWSModal(false)} />}
|
||||
{showDeleteCollectionModal && <DeleteCollection collection={collection} onClose={() => setShowDeleteCollectionModal(false)} />}
|
||||
{showRemoveLocalCollectionModal && <RemoveLocalCollection collection={collection} onClose={() => setShowRemoveLocalCollectionModal(false)} />}
|
||||
<div className="flex py-1 collection-name items-center">
|
||||
<div className="flex flex-grow items-center" onClick={handleClick}>
|
||||
<IconChevronRight size={16} strokeWidth={2} className={iconClassName} style={{ width: 16, color: 'rgb(160 160 160)' }} />
|
||||
{showRemoveCollectionModal && <RemoveCollection collection={collection} onClose={() => setShowRemoveCollectionModal(false)} />}
|
||||
{showRunCollectionModal && <RunCollectionItem collection={collection} onClose={() => setShowRunCollectionModal(false)} />}
|
||||
<div className="flex py-1 collection-name items-center" ref={drop}>
|
||||
<div className="flex flex-grow items-center overflow-hidden" onClick={handleClick}>
|
||||
<IconChevronRight size={16} strokeWidth={2} className={iconClassName} style={{ width: 16, minWidth:16, color: 'rgb(160 160 160)' }} />
|
||||
<div className="ml-1" id="sidebar-collection-name">{collection.name}</div>
|
||||
</div>
|
||||
<div className="collection-actions">
|
||||
@@ -104,17 +125,24 @@ const Collection = ({ collection, searchText }) => {
|
||||
>
|
||||
New Folder
|
||||
</div>
|
||||
{!isLocal ? (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRenameCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRunCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRenameCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
@@ -124,38 +152,15 @@ const Collection = ({ collection, searchText }) => {
|
||||
>
|
||||
Export
|
||||
</div>
|
||||
{!isLocal ? (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRemoveCollectionFromWSModal(true);
|
||||
}}
|
||||
>
|
||||
Remove from Workspace
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRemoveLocalCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
)}
|
||||
{!isLocal ? (
|
||||
<div
|
||||
className="dropdown-item delete-collection"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowDeleteCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowRemoveCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,14 +168,13 @@ const Collection = ({ collection, searchText }) => {
|
||||
<div>
|
||||
{!collectionIsCollapsed ? (
|
||||
<div>
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
|
||||
{folderItems && folderItems.length
|
||||
? folderItems.map((i) => {
|
||||
{requestItems && requestItems.length
|
||||
? requestItems.map((i) => {
|
||||
return <CollectionItem key={i.uid} item={i} collection={collection} searchText={searchText} />;
|
||||
})
|
||||
: null}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useTheme } from '../../../../providers/Theme';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
|
||||
import toast from 'react-hot-toast';
|
||||
import styled from 'styled-components';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const LinkStyle = styled.span`
|
||||
color: ${(props) => props.theme['text-link']};
|
||||
`;
|
||||
|
||||
const CreateOrAddCollection = () => {
|
||||
const { theme } = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [addCollectionToWSModalOpen, setAddCollectionToWSModalOpen] = useState(false);
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
|
||||
const handleCreateCollection = (values) => {
|
||||
setCreateCollectionModalOpen(false);
|
||||
dispatch(createCollection(values.collectionName))
|
||||
.then(() => {
|
||||
toast.success('Collection created');
|
||||
})
|
||||
.catch(() => toast.error('An error occured while creating the collection'));
|
||||
};
|
||||
|
||||
const handleAddCollectionToWorkspace = (collectionUid) => {
|
||||
setAddCollectionToWSModalOpen(false);
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collectionUid))
|
||||
.then(() => {
|
||||
toast.success('Collection added to workspace');
|
||||
})
|
||||
.catch(() => toast.error('An error occured while adding collection to workspace'));
|
||||
};
|
||||
|
||||
const CreateLink = () => (
|
||||
<LinkStyle className="underline text-link cursor-pointer" theme={theme} onClick={() => setCreateCollectionModalOpen(true)}>
|
||||
Create
|
||||
</LinkStyle>
|
||||
);
|
||||
const AddLink = () => (
|
||||
<LinkStyle className="underline text-link cursor-pointer" theme={theme} onClick={() => setAddCollectionToWSModalOpen(true)}>
|
||||
Add
|
||||
</LinkStyle>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-2 mt-4">
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} handleConfirm={handleCreateCollection} /> : null}
|
||||
|
||||
{addCollectionToWSModalOpen ? (
|
||||
<SelectCollection title="Add Collection to Workspace" onClose={() => setAddCollectionToWSModalOpen(false)} onSelect={handleAddCollectionToWorkspace} />
|
||||
) : null}
|
||||
|
||||
<div className="text-xs text-center">
|
||||
<div>No collections found.</div>
|
||||
<div className="mt-2">
|
||||
<CreateLink /> or <AddLink /> Collection to Workspace.
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateOrAddCollection;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useState } from 'react';
|
||||
import { useTheme } from '../../../../providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { openCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import toast from 'react-hot-toast';
|
||||
import styled from 'styled-components';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const LinkStyle = styled.span`
|
||||
color: ${(props) => props.theme['text-link']};
|
||||
`;
|
||||
|
||||
const CreateOrOpenCollection = () => {
|
||||
const { theme } = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch((err) => console.log(err) && toast.error('An error occured while opening the collection'));
|
||||
};
|
||||
const CreateLink = () => (
|
||||
<LinkStyle className="underline text-link cursor-pointer" theme={theme} onClick={() => setCreateCollectionModalOpen(true)}>
|
||||
Create
|
||||
</LinkStyle>
|
||||
);
|
||||
const OpenLink = () => (
|
||||
<LinkStyle className="underline text-link cursor-pointer" theme={theme} onClick={() => handleOpenCollection(true)}>
|
||||
Open
|
||||
</LinkStyle>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-2 mt-4">
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
|
||||
<div className="text-xs text-center">
|
||||
<div>No collections found.</div>
|
||||
<div className="mt-2">
|
||||
<CreateLink /> or <OpenLink /> Collection.
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateOrOpenCollection;
|
||||
@@ -1,21 +1,18 @@
|
||||
import React from 'react';
|
||||
import filter from 'lodash/filter';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { IconFiles } from '@tabler/icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { isLocalCollection } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const SelectCollection = ({ onClose, onSelect, title }) => {
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const collectionsToDisplay = filter(collections, (c) => !isLocalCollection(c));
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={title || 'Select Collection'} hideFooter={true} handleCancel={onClose}>
|
||||
<ul className="mb-2">
|
||||
{collectionsToDisplay && collectionsToDisplay.length ? (
|
||||
collectionsToDisplay.map((c) => (
|
||||
{collections && collections.length ? (
|
||||
collections.map((c) => (
|
||||
<div className="collection" key={c.uid} onClick={() => onSelect(c.uid)}>
|
||||
<IconFiles size={18} strokeWidth={1.5} /> <span className="ml-2">{c.name}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-workspace {
|
||||
.collections-badge {
|
||||
margin-inline: 0.5rem;
|
||||
background-color: ${(props) => props.theme.sidebar.workspace.bg};
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 5px;
|
||||
|
||||
.caret {
|
||||
@@ -12,10 +12,6 @@ const Wrapper = styled.div`
|
||||
fill: rgb(140, 140, 140);
|
||||
}
|
||||
}
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,36 +1,80 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import find from 'lodash/find';
|
||||
import filter from 'lodash/filter';
|
||||
import Collection from './Collection';
|
||||
import CreateOrAddCollection from './CreateOrAddCollection';
|
||||
import { findCollectionInWorkspace } from 'utils/workspaces';
|
||||
import { isLocalCollection } from 'utils/collections';
|
||||
|
||||
const Collections = ({ searchText }) => {
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const activeWorkspace = find(workspaces, (w) => w.uid === activeWorkspaceUid);
|
||||
|
||||
if (!activeWorkspace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const collectionToDisplay = filter(collections, (c) => findCollectionInWorkspace(activeWorkspace, c.uid) && !isLocalCollection(c));
|
||||
|
||||
if (!collectionToDisplay || !collectionToDisplay.length) {
|
||||
return <CreateOrAddCollection />;
|
||||
}
|
||||
import { IconSearch, IconFolders } from '@tabler/icons';
|
||||
import Collection from '../Collections/Collection';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import CreateOrOpenCollection from './CreateOrOpenCollection';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
|
||||
const CollectionsBadge = () => {
|
||||
return (
|
||||
<div className="mt-4 flex flex-col">
|
||||
{collectionToDisplay && collectionToDisplay.length
|
||||
? collectionToDisplay.map((c) => {
|
||||
return <Collection searchText={searchText} collection={c} key={c.uid} />;
|
||||
})
|
||||
: null}
|
||||
<div className="items-center mt-2 relative">
|
||||
<div className="collections-badge flex items-center pl-2 pr-2 py-1 select-none">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Collections</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Collections = () => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
|
||||
if (!collections || !collections.length) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<CollectionsBadge />
|
||||
<CreateOrOpenCollection />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
|
||||
<CollectionsBadge />
|
||||
|
||||
<div className="mt-4 relative collection-filter px-2">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<span className="text-gray-500 sm:text-sm">
|
||||
<IconSearch size={16} strokeWidth={1.5} />
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
id="search"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="block w-full pl-7 py-1 sm:text-sm"
|
||||
placeholder="search"
|
||||
onChange={(e) => setSearchText(e.target.value.toLowerCase())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col">
|
||||
{collections && collections.length
|
||||
? collections.map((c) => {
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend} key={c.uid}>
|
||||
<Collection searchText={searchText} collection={c} key={c.uid} />
|
||||
</DndProvider>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Collections;
|
||||
|
||||
|
||||
@@ -2,29 +2,30 @@ import React, { useRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { browserLocalDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import { createCollection, createLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { createCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const CreateCollection = ({ onClose, isLocal }) => {
|
||||
const CreateCollection = ({ onClose }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const isPlatformElectron = isElectron();
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
collectionName: '',
|
||||
collectionFolderName: '',
|
||||
collectionLocation: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
collectionName: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('name is required')
|
||||
collectionName: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('collection name is required'),
|
||||
collectionFolderName: Yup.string().min(1, 'must be atleast 1 characters').max(50, 'must be 50 characters or less').required('folder name is required'),
|
||||
collectionLocation: Yup.string().required('location is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
const action = isLocal && isPlatformElectron ? createLocalCollection : createCollection;
|
||||
dispatch(action(values.collectionName, values.collectionLocation))
|
||||
dispatch(createCollection(values.collectionName, values.collectionFolderName, values.collectionLocation))
|
||||
.then(() => {
|
||||
toast.success('Collection created');
|
||||
onClose();
|
||||
@@ -34,7 +35,7 @@ const CreateCollection = ({ onClose, isLocal }) => {
|
||||
});
|
||||
|
||||
const browse = () => {
|
||||
dispatch(browserLocalDirectory())
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
})
|
||||
@@ -56,8 +57,9 @@ const CreateCollection = ({ onClose, isLocal }) => {
|
||||
<Modal size="sm" title="Create Collection" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="collectionName" className="block font-semibold">
|
||||
Name
|
||||
<label htmlFor="collectionName" className="flex items-center">
|
||||
<span className='font-semibold'>Name</span>
|
||||
<Tooltip text="Name of the collection" tooltipId="collection-name"/>
|
||||
</label>
|
||||
<input
|
||||
id="collection-name"
|
||||
@@ -74,37 +76,52 @@ const CreateCollection = ({ onClose, isLocal }) => {
|
||||
/>
|
||||
{formik.touched.collectionName && formik.errors.collectionName ? <div className="text-red-500">{formik.errors.collectionName}</div> : null}
|
||||
|
||||
{isLocal && isPlatformElectron ? (
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{isLocal && isPlatformElectron && formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<label htmlFor="collectionFolderName" className="flex items-center mt-3">
|
||||
<span className='font-semibold'>Folder Name</span>
|
||||
<Tooltip text="Name of the folder where your collection is stored" tooltipId="collection-folder-name"/>
|
||||
</label>
|
||||
<input
|
||||
id="collection-folder-name"
|
||||
type="text"
|
||||
name="collectionFolderName"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={formik.handleChange}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionFolderName || ''}
|
||||
/>
|
||||
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? <div className="text-red-500">{formik.errors.collectionFolderName}</div> : null}
|
||||
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
</>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
|
||||
{isLocal && isPlatformElectron ? (
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { collectionImported } from 'providers/ReduxStore/slices/collections';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ImportCollection = ({ onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
|
||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
const handleImportBrunoCollection = () => {
|
||||
importBrunoCollection()
|
||||
.then((collection) => {
|
||||
dispatch(collectionImported({ collection: collection }));
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid));
|
||||
toast.success('Collection imported successfully');
|
||||
onClose();
|
||||
handleSubmit(collection);
|
||||
})
|
||||
.catch((err) => toastError(err, 'Import collection failed'));
|
||||
};
|
||||
@@ -26,10 +16,7 @@ const ImportCollection = ({ onClose }) => {
|
||||
const handleImportPostmanCollection = () => {
|
||||
importPostmanCollection()
|
||||
.then((collection) => {
|
||||
dispatch(collectionImported({ collection: collection }));
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid));
|
||||
toast.success('Postman Collection imported successfully');
|
||||
onClose();
|
||||
handleSubmit(collection);
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import collection failed'));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
collectionLocation: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
collectionLocation: Yup.string().min(1, 'must be atleast 1 characters').max(500, 'must be 500 characters or less').required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
console.log('here');
|
||||
handleSubmit(values.collectionLocation);
|
||||
}
|
||||
});
|
||||
|
||||
const browse = () => {
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
})
|
||||
.catch((error) => {
|
||||
formik.setFieldValue('collectionLocation', '');
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" confirmText="Import" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="collectionName" className="block font-semibold">
|
||||
Name
|
||||
</label>
|
||||
<div className='mt-2'>{collectionName}</div>
|
||||
|
||||
<>
|
||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
</>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportCollectionLocation;
|
||||
@@ -1,26 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-workspace {
|
||||
margin-inline: 0.5rem;
|
||||
background-color: ${(props) => props.theme.sidebar.workspace.bg};
|
||||
border-radius: 5px;
|
||||
|
||||
.caret {
|
||||
margin-left: 0.25rem;
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140, 140, 140);
|
||||
}
|
||||
}
|
||||
|
||||
.muted-message {
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
|
||||
}
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,79 +0,0 @@
|
||||
import React, { useState, useRef, forwardRef } from 'react';
|
||||
import filter from 'lodash/filter';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { openLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconArrowForwardUp, IconCaretDown, IconFolders, IconPlus } from '@tabler/icons';
|
||||
import Collection from '../Collections/Collection';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import { isLocalCollection } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const LocalCollections = ({ searchText }) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
|
||||
const collectionToDisplay = filter(collections, (c) => isLocalCollection(c));
|
||||
|
||||
if (!collectionToDisplay || !collectionToDisplay.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="current-workspace flex justify-between items-center pl-2 pr-2 py-1 select-none">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Local Collections</span>
|
||||
</div>
|
||||
<IconCaretDown className="caret" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const handleOpenLocalCollection = () => {
|
||||
dispatch(openLocalCollection()).catch((err) => console.log(err) && toast.error('An error occured while opening the local collection'));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{createCollectionModalOpen ? <CreateCollection isLocal={true} onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
|
||||
<div className="items-center cursor-pointer mt-6 relative">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div className="dropdown-item" onClick={() => setCreateCollectionModalOpen(true)}>
|
||||
<div className="pr-2 text-gray-600">
|
||||
<IconPlus size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span>Create Collection</span>
|
||||
</div>
|
||||
<div className="dropdown-item" onClick={handleOpenLocalCollection}>
|
||||
<div className="pr-2 text-gray-600">
|
||||
<IconArrowForwardUp size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span>Open Collection</span>
|
||||
</div>
|
||||
|
||||
<div className="px-2 pt-2 muted-message" style={{ fontSize: 10 }}>
|
||||
Note: Local collections are not tied to a workspace
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col">
|
||||
{collectionToDisplay && collectionToDisplay.length
|
||||
? collectionToDisplay.map((c) => {
|
||||
return <Collection searchText={searchText} collection={c} key={c.uid} />;
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocalCollections;
|
||||
@@ -27,18 +27,12 @@ const MenuBar = () => {
|
||||
{openTheme && <SwitchTheme onClose={() => setOpenTheme(false)} />}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Link href="/">
|
||||
{/* Todo: Fix this: Clicking on this crashes the app */}
|
||||
{/* <Link href="/">
|
||||
<div className={getClassName('/')}>
|
||||
<IconCode size={28} strokeWidth={1.5} />
|
||||
</div>
|
||||
</Link>
|
||||
{!isPlatformElectron ? (
|
||||
<Link href="/collections">
|
||||
<div className={getClassName('/collections')}>
|
||||
<IconFiles size={28} strokeWidth={1.5} />
|
||||
</div>
|
||||
</Link>
|
||||
) : null}
|
||||
</Link> */}
|
||||
{/* <div className="menu-item">
|
||||
<IconUsers size={28} strokeWidth={1.5}/>
|
||||
</div> */}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.local-collections-unavailable {
|
||||
padding: 0.35rem 0.6rem;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.seperator};
|
||||
font-size: 11px;
|
||||
}
|
||||
.collection-dropdown {
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
|
||||
|
||||
@@ -2,27 +2,36 @@ import toast from 'react-hot-toast';
|
||||
import Bruno from 'components/Bruno';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
|
||||
|
||||
import { IconDots } from '@tabler/icons';
|
||||
import { IconFolders } from '@tabler/icons';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import { useState, forwardRef, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { showHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { openLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TitleBar = () => {
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const [addCollectionToWSModalOpen, setAddCollectionToWSModalOpen] = useState(false);
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const isPlatformElectron = isElectron();
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleImportCollection = (collection) => {
|
||||
setImportedCollection(collection);
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
|
||||
const handleImportCollectionLocation = (collectionLocation) => {
|
||||
dispatch(importCollection(importedCollection, collectionLocation));
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportedCollection(null);
|
||||
toast.success('Collection imported successfully');
|
||||
};
|
||||
|
||||
const menuDropdownTippyRef = useRef();
|
||||
const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
@@ -35,27 +44,21 @@ const TitleBar = () => {
|
||||
|
||||
const handleTitleClick = () => dispatch(showHomePage());
|
||||
|
||||
const handleOpenLocalCollection = () => {
|
||||
dispatch(openLocalCollection()).catch((err) => console.log(err) && toast.error('An error occured while opening the local collection'));
|
||||
};
|
||||
|
||||
const handleAddCollectionToWorkspace = (collectionUid) => {
|
||||
setAddCollectionToWSModalOpen(false);
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collectionUid))
|
||||
.then(() => {
|
||||
toast.success('Collection added to workspace');
|
||||
})
|
||||
.catch(() => toast.error('An error occured while adding collection to workspace'));
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch((err) => console.log(err) && toast.error('An error occured while opening the collection'));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-2 py-2">
|
||||
{createCollectionModalOpen ? <CreateCollection isLocal={createCollectionModalOpen === 'local' ? true : false} onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
{importCollectionModalOpen ? <ImportCollection onClose={() => setImportCollectionModalOpen(false)} /> : null}
|
||||
|
||||
{addCollectionToWSModalOpen ? (
|
||||
<SelectCollection title="Add Collection to Workspace" onClose={() => setAddCollectionToWSModalOpen(false)} onSelect={handleAddCollectionToWorkspace} />
|
||||
) : null}
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
{importCollectionModalOpen ? <ImportCollection onClose={() => setImportCollectionModalOpen(false)} handleSubmit={handleImportCollection} /> : null}
|
||||
{importCollectionLocationModalOpen ? (
|
||||
<ImportCollectionLocation
|
||||
collectionName={importedCollection.name}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
): null}
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center cursor-pointer" onClick={handleTitleClick}>
|
||||
@@ -73,12 +76,21 @@ const TitleBar = () => {
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setCreateCollectionModalOpen(true);
|
||||
menuDropdownTippyRef.current.hide();
|
||||
}}
|
||||
>
|
||||
Create Collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
handleOpenCollection();
|
||||
menuDropdownTippyRef.current.hide();
|
||||
}}
|
||||
>
|
||||
Open Collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
@@ -88,49 +100,6 @@ const TitleBar = () => {
|
||||
>
|
||||
Import Collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setAddCollectionToWSModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Add Collection to Workspace
|
||||
</div>
|
||||
{isPlatformElectron ? (
|
||||
<>
|
||||
<div className="font-medium label-item font-medium local-collection-label">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Local Collections</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
setCreateCollectionModalOpen('local');
|
||||
menuDropdownTippyRef.current.hide();
|
||||
}}
|
||||
>
|
||||
Create Local Collection
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
handleOpenLocalCollection();
|
||||
menuDropdownTippyRef.current.hide();
|
||||
}}
|
||||
>
|
||||
Open Local Collection
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center select-none text-xs local-collections-unavailable">
|
||||
Note: Local collections are only available on the desktop app.
|
||||
</div>
|
||||
)}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import MenuBar from './MenuBar';
|
||||
import TitleBar from './TitleBar';
|
||||
import Collections from './Collections';
|
||||
import LocalCollections from './LocalCollections';
|
||||
import StyledWrapper, { BottomWrapper, VersionNumber } from './StyledWrapper';
|
||||
import WorkspaceSelector from 'components/Workspaces/WorkspaceSelector';
|
||||
import GitHubButton from 'react-github-btn'
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconSearch, IconChevronsRight } from '@tabler/icons';
|
||||
import { IconChevronsRight } from '@tabler/icons';
|
||||
import { updateLeftSidebarWidth, updateIsDragging, toggleLeftMenuBar } from 'providers/ReduxStore/slices/app';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const MIN_LEFT_SIDEBAR_WIDTH = 222;
|
||||
const MAX_LEFT_SIDEBAR_WIDTH = 600;
|
||||
@@ -19,9 +19,12 @@ const Sidebar = () => {
|
||||
|
||||
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
|
||||
|
||||
const {
|
||||
storedTheme
|
||||
} = useTheme();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (dragging) {
|
||||
@@ -73,58 +76,48 @@ const Sidebar = () => {
|
||||
setAsideWidth(leftSidebarWidth);
|
||||
}, [leftSidebarWidth]);
|
||||
|
||||
const leftMenuBarWidth = leftMenuBarOpen ? 48 : 0;
|
||||
const collectionsWidth = asideWidth - leftMenuBarWidth;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex relative">
|
||||
<aside style={{ width: `${asideWidth}px`, minWidth: `${asideWidth}px` }}>
|
||||
<aside>
|
||||
<div className="flex flex-row h-full w-full">
|
||||
{leftMenuBarOpen && <MenuBar />}
|
||||
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-col w-full" style={{width: collectionsWidth}}>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<TitleBar />
|
||||
<WorkspaceSelector />
|
||||
|
||||
<div className="mt-4 relative collection-filter px-2">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<span className="text-gray-500 sm:text-sm">
|
||||
<IconSearch size={16} strokeWidth={1.5} />
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
id="search"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="block w-full pl-7 py-1 sm:text-sm"
|
||||
placeholder="search"
|
||||
onChange={(e) => setSearchText(e.target.value.toLowerCase())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Collections searchText={searchText} />
|
||||
<LocalCollections searchText={searchText} />
|
||||
<Collections />
|
||||
</div>
|
||||
|
||||
<div className="footer flex px-1 py-2 items-center cursor-pointer select-none">
|
||||
<div className="flex items-center ml-1 text-xs ">
|
||||
{!leftMenuBarOpen && <IconChevronsRight size={24} strokeWidth={1.5} className="mr-2 hover:text-gray-700" onClick={() => dispatch(toggleLeftMenuBar())} />}
|
||||
{/* <IconLayoutGrid size={20} strokeWidth={1.5} className="mr-2"/> */}
|
||||
{/* Need to ut github stars link here */}
|
||||
</div>
|
||||
<div className="pl-1">
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=usebruno&repo=bruno&type=star&count=true"
|
||||
frameBorder="0"
|
||||
scrolling="0"
|
||||
width="100"
|
||||
height="20"
|
||||
title="GitHub"
|
||||
></iframe>
|
||||
<div className="pl-1" style={{position: 'relative', top: '3px'}}>
|
||||
{storedTheme === 'dark' ? (
|
||||
<GitHubButton
|
||||
href="https://github.com/usebruno/bruno"
|
||||
data-color-scheme="no-preference: dark; light: dark; dark: light;"
|
||||
data-show-count="true"
|
||||
aria-label="Star usebruno/bruno on GitHub"
|
||||
>
|
||||
Star
|
||||
</GitHubButton>
|
||||
) : (
|
||||
<GitHubButton
|
||||
href="https://github.com/usebruno/bruno"
|
||||
data-color-scheme="no-preference: light; light: light; dark: light;"
|
||||
data-show-count="true"
|
||||
aria-label="Star usebruno/bruno on GitHub"
|
||||
>
|
||||
Star
|
||||
</GitHubButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.3.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.9.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
|
||||
.CodeMirror {
|
||||
background: transparent;
|
||||
height: 34px;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
overflow: hidden;
|
||||
|
||||
.CodeMirror-vscrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.CodeMirror-hscrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-cursor {
|
||||
height: 20px !important;
|
||||
margin-top: 5px !important;
|
||||
border-left: 1px solid ${(props) => props.theme.text} !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: Inter, sans-serif !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.CodeMirror-line {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
128
packages/bruno-app/src/components/SingleLineEditor/index.js
Normal file
128
packages/bruno-app/src/components/SingleLineEditor/index.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import React, { Component } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
|
||||
class SingleLineEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Keep a cached version of the value, this cache will be updated when the
|
||||
// editor is updated, which can later be used to protect the editor from
|
||||
// unnecessary updates during the update lifecycle.
|
||||
this.cachedValue = props.value || '';
|
||||
this.editorRef = React.createRef();
|
||||
this.variables = {};
|
||||
}
|
||||
componentDidMount() {
|
||||
// Initialize CodeMirror as a single line editor
|
||||
this.editor = CodeMirror(this.editorRef.current, {
|
||||
lineWrapping: false,
|
||||
lineNumbers: false,
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
mode: "brunovariables",
|
||||
brunoVarInfo: {
|
||||
variables: getAllVariables(this.props.collection),
|
||||
},
|
||||
extraKeys: {
|
||||
"Enter": () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
"Ctrl-Enter": () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
"Cmd-Enter": () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
"Alt-Enter": () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
"Shift-Enter": () => {
|
||||
if (this.props.onRun) {
|
||||
this.props.onRun();
|
||||
}
|
||||
},
|
||||
'Cmd-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Ctrl-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Cmd-F': () => {},
|
||||
'Ctrl-F': () => {},
|
||||
'Tab': () => {}
|
||||
},
|
||||
});
|
||||
this.editor.setValue(this.props.value || '');
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
}
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.cachedValue = this.editor.getValue();
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(this.cachedValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// Ensure the changes caused by this update are not interpretted as
|
||||
// user-input changes which could otherwise result in an infinite
|
||||
// event loop.
|
||||
this.ignoreChangeEvent = true;
|
||||
|
||||
let variables = getAllVariables(this.props.collection);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.editor.options.brunoVarInfo.variables = variables;
|
||||
this.addOverlay();
|
||||
}
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.editor.getWrapperElement().remove();
|
||||
}
|
||||
|
||||
addOverlay = () => {
|
||||
let variables = getAllVariables(this.props.collection);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, "text/plain");
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledWrapper ref={this.editorRef} className="single-line-editor"></StyledWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default SingleLineEditor;
|
||||
16
packages/bruno-app/src/components/Tooltip/index.js
Normal file
16
packages/bruno-app/src/components/Tooltip/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Tooltip as ReactTooltip } from 'react-tooltip';
|
||||
|
||||
const Tooltip = ({ text, tooltipId }) => {
|
||||
return (
|
||||
<>
|
||||
<svg tabIndex="-1" id={tooltipId} xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" className="inline-block ml-2 cursor-pointer" viewBox="0 0 16 16" style={{marginTop: 1}}>
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
|
||||
</svg>
|
||||
<ReactTooltip anchorId={tooltipId} content={text} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
@@ -0,0 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: absolute;
|
||||
min-width: fit-content;
|
||||
font-size: 14px;
|
||||
top: 36px;
|
||||
right: 0;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
background-color: ${(props) => props.theme.variables.bg};
|
||||
|
||||
.popover {
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, {useRef} from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import useOnClickOutside from 'hooks/useOnClickOutside';
|
||||
|
||||
const PopOver = ({
|
||||
children,
|
||||
iconRef,
|
||||
handleClose
|
||||
}) => {
|
||||
const popOverRef = useRef(null);
|
||||
|
||||
useOnClickOutside(popOverRef, (e) => {
|
||||
if(iconRef && iconRef.current) {
|
||||
if (e.target == iconRef.current || iconRef.current.contains(e.target)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
handleClose();
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="popover" ref={popOverRef}>
|
||||
<div className="popover-content">{children}</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopOver;
|
||||
@@ -0,0 +1,15 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.view-environment {
|
||||
width: 1rem;
|
||||
font-size: 10px;
|
||||
}
|
||||
`
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.variable-name {
|
||||
color: ${(props) => props.theme.variables.name.color};
|
||||
}
|
||||
|
||||
.variable-name{
|
||||
min-width:180px;
|
||||
}
|
||||
|
||||
.variable-value {
|
||||
max-width: 600px;
|
||||
inline-size: 600px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
`
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { uuid } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const VariablesTable = ({ variables, collectionVariables }) => {
|
||||
const collectionVars = [];
|
||||
|
||||
forOwn(cloneDeep(collectionVariables), (value, key) => {
|
||||
collectionVars.push({
|
||||
uid: uuid(),
|
||||
name: key,
|
||||
value: value
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className='mb-2 font-medium'>Environment Variables</div>
|
||||
{(variables && variables.length) ? variables.map((variable) => {
|
||||
return (
|
||||
<div key={variable.uid} className="flex">
|
||||
<div className='variable-name text-yellow-600 text-right pr-2'>{variable.name}</div>
|
||||
<div className='variable-value pl-2 whitespace-normal text-left flex-grow'>{variable.value}</div>
|
||||
</div>
|
||||
);
|
||||
}) : null}
|
||||
|
||||
<div className='mt-2 font-medium'>Collection Variables</div>
|
||||
{(collectionVars && collectionVars.length) ? collectionVars.map((variable) => {
|
||||
return (
|
||||
<div key={variable.uid} className="flex">
|
||||
<div className='variable-name text-yellow-600 text-right pr-2'>{variable.name}</div>
|
||||
<div className='variable-value pl-2 whitespace-normal text-left flex-grow'>{variable.value}</div>
|
||||
</div>
|
||||
);
|
||||
}) : null}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default VariablesTable;
|
||||
46
packages/bruno-app/src/components/VariablesView/index.js
Normal file
46
packages/bruno-app/src/components/VariablesView/index.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import filter from 'lodash/filter';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import VariablesTable from './VariablesTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import PopOver from './Popover';
|
||||
import { IconEye } from '@tabler/icons';
|
||||
|
||||
const VariablesView = ({collection}) => {
|
||||
const iconRef = useRef(null);
|
||||
const [popOverOpen, setPopOverOpen] = useState(false);
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
const variables = get(environment, 'variables', []);
|
||||
const enabledVariables = filter(variables, (variable) => variable.enabled);
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="mr-2 server-syncstatus-icon"
|
||||
ref={iconRef}
|
||||
>
|
||||
<div className="flex p-1 items-center"
|
||||
onClick={() => setPopOverOpen(true)}
|
||||
onMouseEnter={() => setPopOverOpen(true)}
|
||||
onMouseLeave={() => setPopOverOpen(false)}
|
||||
>
|
||||
<div className='cursor-pointer view-environment'>
|
||||
<IconEye size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
{popOverOpen && (
|
||||
<PopOver
|
||||
iconRef={iconRef}
|
||||
handleClose={() => setPopOverOpen(false)}
|
||||
>
|
||||
<div className="px-2 py-1">
|
||||
{(enabledVariables && enabledVariables.length) ? <VariablesTable variables={enabledVariables} collectionVariables={collection.collectionVariables}/> : 'No variables found'}
|
||||
</div>
|
||||
</PopOver>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
)
|
||||
};
|
||||
|
||||
export default VariablesView;
|
||||
@@ -1,67 +1,56 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { collectionImported } from 'providers/ReduxStore/slices/collections';
|
||||
import { openLocalCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addCollectionToWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { IconBrandGithub, IconPlus, IconUpload, IconFiles, IconFolders, IconPlayerPlay, IconBrandChrome, IconSpeakerphone, IconDeviceDesktop } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconBrandGithub, IconPlus, IconUpload, IconFolders, IconSpeakerphone } from '@tabler/icons';
|
||||
|
||||
import Bruno from 'components/Bruno';
|
||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||
import SelectCollection from 'components/Sidebar/Collections/SelectCollection';
|
||||
import { importSampleCollection } from 'utils/importers/bruno-collection';
|
||||
import ImportCollection from 'components/Sidebar/ImportCollection';
|
||||
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Welcome = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [importedCollection, setImportedCollection] = useState(null);
|
||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||
const [addCollectionToWSModalOpen, setAddCollectionToWSModalOpen] = useState(false);
|
||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||
const { activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const isPlatformElectron = isElectron();
|
||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||
|
||||
const handleAddCollectionToWorkspace = (collectionUid) => {
|
||||
setAddCollectionToWSModalOpen(false);
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collectionUid))
|
||||
.then(() => {
|
||||
toast.success('Collection added to workspace');
|
||||
})
|
||||
.catch(() => toast.error('An error occured while adding collection to workspace'));
|
||||
const handleOpenCollection = () => {
|
||||
dispatch(openCollection()).catch((err) => console.log(err) && toast.error('An error occured while opening the collection'));
|
||||
};
|
||||
|
||||
const handleImportSampleCollection = () => {
|
||||
importSampleCollection()
|
||||
.then((collection) => {
|
||||
dispatch(collectionImported({ collection: collection }));
|
||||
dispatch(addCollectionToWorkspace(activeWorkspaceUid, collection.uid));
|
||||
})
|
||||
.then(() => toast.success('Sample Collection loaded successfully'))
|
||||
.catch((err) => {
|
||||
toast.error('Load sample collection failed');
|
||||
console.log(err);
|
||||
});
|
||||
const handleImportCollection = (collection) => {
|
||||
setImportedCollection(collection);
|
||||
setImportCollectionModalOpen(false);
|
||||
setImportCollectionLocationModalOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenLocalCollection = () => {
|
||||
dispatch(openLocalCollection()).catch((err) => console.log(err) && toast.error('An error occured while opening the local collection'));
|
||||
const handleImportCollectionLocation = (collectionLocation) => {
|
||||
dispatch(importCollection(importedCollection, collectionLocation));
|
||||
setImportCollectionLocationModalOpen(false);
|
||||
setImportedCollection(null);
|
||||
toast.success('Collection imported successfully');
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="pb-4 px-6 mt-6">
|
||||
{createCollectionModalOpen ? <CreateCollection isLocal={createCollectionModalOpen === 'local' ? true : false} onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
{importCollectionModalOpen ? <ImportCollection onClose={() => setImportCollectionModalOpen(false)} /> : null}
|
||||
|
||||
{addCollectionToWSModalOpen ? (
|
||||
<SelectCollection title="Add Collection to Workspace" onClose={() => setAddCollectionToWSModalOpen(false)} onSelect={handleAddCollectionToWorkspace} />
|
||||
) : null}
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
{importCollectionModalOpen ? <ImportCollection onClose={() => setImportCollectionModalOpen(false)} handleSubmit={handleImportCollection} /> : null}
|
||||
{importCollectionLocationModalOpen ? (
|
||||
<ImportCollectionLocation
|
||||
collectionName={importedCollection.name}
|
||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||
handleSubmit={handleImportCollectionLocation}
|
||||
/>
|
||||
): null}
|
||||
|
||||
<div className="">
|
||||
<Bruno width={50} />
|
||||
</div>
|
||||
<div className="text-xl font-semibold select-none">bruno</div>
|
||||
<div className="mt-4">Local-first, Opensource API Client.</div>
|
||||
<div className="mt-4">Opensource IDE for exploring and testing api's</div>
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10">Collections</div>
|
||||
<div className="mt-4 flex items-center collection-options select-none">
|
||||
@@ -71,58 +60,22 @@ const Welcome = () => {
|
||||
Create Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={() => setAddCollectionToWSModalOpen(true)}>
|
||||
<IconFiles size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="add-collection">
|
||||
Add Collection to Workspace
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={handleOpenCollection}>
|
||||
<IconFolders size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">
|
||||
Open Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
|
||||
<IconUpload size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="import-collection">Import Collection</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6" onClick={handleImportSampleCollection}>
|
||||
<IconPlayerPlay size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" id="load-sample-collection">Load Sample Collection</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10 pt-6">Local Collections</div>
|
||||
{isPlatformElectron ? (
|
||||
<div className="mt-4 flex items-center collection-options select-none">
|
||||
<div className="flex items-center">
|
||||
<IconPlus size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" onClick={() => setCreateCollectionModalOpen('local')}>
|
||||
Create Collection
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center ml-6">
|
||||
<IconFolders size={18} strokeWidth={2} />
|
||||
<span className="label ml-2" onClick={handleOpenLocalCollection}>
|
||||
Open Collection
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="muted mt-4 flex items-center collection-options select-none text-gray-600 text-xs">Local collections are only available on the desktop app.</div>
|
||||
)}
|
||||
|
||||
<div className="uppercase font-semibold heading mt-10 pt-6">Links</div>
|
||||
<div className="mt-4 flex flex-col collection-options select-none">
|
||||
<div>
|
||||
<a href="https://www.usebruno.com/downloads" target="_blank" className="flex items-center">
|
||||
<IconBrandChrome size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Chrome Extension</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://www.usebruno.com/downloads" target="_blank" className="flex items-center">
|
||||
<IconDeviceDesktop size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Desktop App</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-center">
|
||||
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center">
|
||||
<IconSpeakerphone size={18} strokeWidth={2} />
|
||||
<span className="label ml-2">Report Issues</span>
|
||||
</a>
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { useFormik } from 'formik';
|
||||
import { addWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const AddWorkspace = ({ onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().min(1, 'must be atleast 1 characters').max(30, 'must be 30 characters or less').required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(addWorkspace(values.name))
|
||||
.then(() => {
|
||||
toast.success('Workspace created!');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occured while creating the workspace'));
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => {
|
||||
formik.handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title={'Add Workspace'} confirmText="Add" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
Workspace Name
|
||||
</label>
|
||||
<input
|
||||
id="workspace-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddWorkspace;
|
||||
@@ -1,15 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
button.submit {
|
||||
color: white;
|
||||
background-color: var(--color-background-danger) !important;
|
||||
border: inherit !important;
|
||||
|
||||
&:hover {
|
||||
border: inherit !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { deleteWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DeleteWorkspace = ({ onClose, workspace }) => {
|
||||
const dispatch = useDispatch();
|
||||
const onConfirm = () => {
|
||||
dispatch(deleteWorkspace(workspace.uid))
|
||||
.then(() => {
|
||||
toast.success('Workspace deleted!');
|
||||
onClose();
|
||||
})
|
||||
.catch(toastError);
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={'Delete Workspace'} confirmText="Delete" handleConfirm={onConfirm} handleCancel={onClose}>
|
||||
Are you sure you want to delete <span className="font-semibold">{workspace.name}</span> ?
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteWorkspace;
|
||||
@@ -1,70 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { useFormik } from 'formik';
|
||||
import { renameWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const EditWorkspace = ({ onClose, workspace }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: workspace.name
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().min(1, 'must be atleast 1 characters').max(30, 'must be 30 characters or less').required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(renameWorkspace(values.name, workspace.uid))
|
||||
.then(() => {
|
||||
toast.success('Workspace renamed!');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occured while renaming the workspace'));
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => {
|
||||
formik.handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title={'Rename Workspace'} confirmText="Rename" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
Workspace Name
|
||||
</label>
|
||||
<input
|
||||
id="workspace-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? <div className="text-red-500">{formik.errors.name}</div> : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditWorkspace;
|
||||
@@ -1,17 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
div {
|
||||
padding: 4px 6px;
|
||||
padding-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
div:hover {
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,26 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import EditWorkspace from '../EditWorkspace';
|
||||
import DeleteWorkspace from '../DeleteWorkspace';
|
||||
import { IconEdit, IconTrash } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WorkspaceItem = ({ workspace }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex justify-between items-baseline mb-2" key={workspace.uid}>
|
||||
<li>{workspace.name}</li>
|
||||
<div className="flex gap-x-4">
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
|
||||
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} />
|
||||
</div>
|
||||
{openEditModal && <EditWorkspace onClose={() => setOpenEditModal(false)} workspace={workspace} />}
|
||||
{openDeleteModal && <DeleteWorkspace onClose={() => setOpenDeleteModal(false)} workspace={workspace} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceItem;
|
||||
@@ -1,19 +0,0 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import WorkspaceItem from './WorkspaceItem/index';
|
||||
import AddWorkspace from './AddWorkspace';
|
||||
|
||||
const WorkspaceConfigurer = ({ onClose }) => {
|
||||
const { workspaces } = useSelector((state) => state.workspaces);
|
||||
const [openAddModal, setOpenAddModal] = useState(false);
|
||||
|
||||
return (
|
||||
<Modal size="md" title="Workspaces" confirmText={'+ New Workspace'} handleConfirm={() => setOpenAddModal(true)} handleCancel={onClose} hideCancel={true}>
|
||||
<ul className="mb-2">{workspaces && workspaces.length && workspaces.map((workspace) => <WorkspaceItem workspace={workspace} key={workspace.uid} />)}</ul>
|
||||
{openAddModal && <AddWorkspace onClose={() => setOpenAddModal(false)} />}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceConfigurer;
|
||||
@@ -1,18 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.workspace {
|
||||
padding: 4px 6px;
|
||||
padding-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { IconBox } from '@tabler/icons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WorkspaceSelectModal = ({ onClose, onSelect, title }) => {
|
||||
const { workspaces } = useSelector((state) => state.workspaces);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={title || 'Select Workspace'} hideFooter={true} handleCancel={onClose}>
|
||||
<ul className="mb-2">
|
||||
{workspaces && workspaces.length ? (
|
||||
workspaces.map((w) => (
|
||||
<div className="workspace" key={w.uid} onClick={() => onSelect(w.uid)}>
|
||||
<IconBox size={18} strokeWidth={1.5} /> <span className="ml-2">{w.name}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div>No workspaces found</div>
|
||||
)}
|
||||
</ul>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceSelectModal;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user