mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
243 Commits
workspaces
...
release/v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c82b61209a | ||
|
|
aea4f6934e | ||
|
|
77e47f361b | ||
|
|
5f6be0a82c | ||
|
|
fb6e2816d5 | ||
|
|
65363266c6 | ||
|
|
8182d6cef1 | ||
|
|
8300abe086 | ||
|
|
a3809ce4b9 | ||
|
|
adb46110dd | ||
|
|
7cc4c0993e | ||
|
|
1030d02ac7 | ||
|
|
d616be7271 | ||
|
|
afd49d146f | ||
|
|
97e43c4489 | ||
|
|
f9af22d586 | ||
|
|
8590bacd79 | ||
|
|
a7d1a349e3 | ||
|
|
d03d8f01a1 | ||
|
|
97c700beba | ||
|
|
b6a27bc66c | ||
|
|
76a2889206 | ||
|
|
d506c37516 | ||
|
|
0c4ad0ed60 | ||
|
|
30dbe34e2e | ||
|
|
c83c055654 | ||
|
|
33f47ca5e4 | ||
|
|
475707848e | ||
|
|
2d76c444f6 | ||
|
|
33361ba659 | ||
|
|
1e3a0d9af3 | ||
|
|
2a5ec854cc | ||
|
|
d758645144 | ||
|
|
902d9ff968 | ||
|
|
781def844d | ||
|
|
15065eb2f1 | ||
|
|
481fa4ed12 | ||
|
|
a35b455041 | ||
|
|
bfc8968e24 | ||
|
|
f90f256f5f | ||
|
|
4253080f6b | ||
|
|
7a83641436 | ||
|
|
7105e4e8ed | ||
|
|
849465d62a | ||
|
|
058d2e0e61 | ||
|
|
c03c5eb927 | ||
|
|
156e798f90 | ||
|
|
a1a90d19e8 | ||
|
|
ec40d7fc85 | ||
|
|
5fda0866f8 | ||
|
|
f611c05fe8 | ||
|
|
b3764e1703 | ||
|
|
f961e692ee | ||
|
|
a7237c2e41 | ||
|
|
3ccaf29ddd | ||
|
|
002a5d16eb | ||
|
|
877b4dcf3a | ||
|
|
b9856f8c64 | ||
|
|
e63bac6ce4 | ||
|
|
23e809e827 | ||
|
|
1a4a30c8f2 | ||
|
|
2c973bbd35 | ||
|
|
8e74fa6233 | ||
|
|
1ec8f55a9e | ||
|
|
1ae05dfb0e | ||
|
|
72f186b38a | ||
|
|
ea1002c7a0 | ||
|
|
89ed1da4de | ||
|
|
4d519df8bc | ||
|
|
71413b9154 | ||
|
|
ce9773b7c9 | ||
|
|
8a394cdafc | ||
|
|
ddc88b3b05 | ||
|
|
746a49faed | ||
|
|
2827a6f133 | ||
|
|
1ed9d61ee8 | ||
|
|
ac0b69787d | ||
|
|
ab04850367 | ||
|
|
1c9db0886d | ||
|
|
b75c9fdd6d | ||
|
|
27dff7567c | ||
|
|
8fa8ae5fed | ||
|
|
0848393319 | ||
|
|
76f8bce9ac | ||
|
|
676f8223ec | ||
|
|
36b0a90de3 | ||
|
|
d7cef7aa4e | ||
|
|
5dad137631 | ||
|
|
8b1d59fa74 | ||
|
|
3a6f2f26ee | ||
|
|
6f71717105 | ||
|
|
2b28d37c74 | ||
|
|
7675c1a4d8 | ||
|
|
6afbaa0d91 | ||
|
|
c0ac24d090 | ||
|
|
41b37c7805 | ||
|
|
6d77cacbc4 | ||
|
|
0d536fb365 | ||
|
|
9a78432dc0 | ||
|
|
63d31825ff | ||
|
|
018f39239f | ||
|
|
1b57b6bee6 | ||
|
|
646f63dbeb | ||
|
|
c714e9b5d6 | ||
|
|
f5ed96ad16 | ||
|
|
f40e4d2d79 | ||
|
|
84f572fa88 | ||
|
|
faec95f623 | ||
|
|
cd6ffc2447 | ||
|
|
d37bf7a5ad | ||
|
|
b41f4974f9 | ||
|
|
2446301e41 | ||
|
|
67903f26bc | ||
|
|
1b8eece173 | ||
|
|
1f05ffd469 | ||
|
|
c2acc25461 | ||
|
|
dc9df80638 | ||
|
|
c5abe4122b | ||
|
|
3081c06964 | ||
|
|
8c7ed3fe51 | ||
|
|
ce33cee03d | ||
|
|
d93d1eacdb | ||
|
|
aeb6b12b06 | ||
|
|
41ed51b4e3 | ||
|
|
b85f60e1d6 | ||
|
|
49ffdd1b8f | ||
|
|
f1961a8988 | ||
|
|
4831434e37 | ||
|
|
87c8934c45 | ||
|
|
01d4d3dc2a | ||
|
|
70178f60b3 | ||
|
|
cba164bc9b | ||
|
|
669c99f40a | ||
|
|
9967d863f5 | ||
|
|
3552801ca5 | ||
|
|
6f2804ea0f | ||
|
|
41efa8505b | ||
|
|
f47e9e9304 | ||
|
|
5f88e7d201 | ||
|
|
e4e17b0c74 | ||
|
|
83feffd41d | ||
|
|
7d783d473f | ||
|
|
6a177e17d3 | ||
|
|
3552e7e609 | ||
|
|
7164119695 | ||
|
|
6a05b04676 | ||
|
|
8c1975ba7b | ||
|
|
397ccbb425 | ||
|
|
336496a1d7 | ||
|
|
aadbf8c33f | ||
|
|
c5827dfa72 | ||
|
|
9738a2afb7 | ||
|
|
a1c4113897 | ||
|
|
052d143d6e | ||
|
|
aac219d4cd | ||
|
|
b188a9e9a9 | ||
|
|
6ab8fcb710 | ||
|
|
1cc117ceb9 | ||
|
|
62b8784972 | ||
|
|
5e6444b8b5 | ||
|
|
bc2efb9686 | ||
|
|
678fa88a7c | ||
|
|
80e09d1a26 | ||
|
|
78ee99eab9 | ||
|
|
73124fd715 | ||
|
|
4c1fba611a | ||
|
|
3cfbf890ac | ||
|
|
395aa4246e | ||
|
|
639c8e573f | ||
|
|
7d317a775b | ||
|
|
2eb8db9b45 | ||
|
|
30d2a6d141 | ||
|
|
231776ca4b | ||
|
|
dbd966850c | ||
|
|
dc111ecce2 | ||
|
|
fdff792476 | ||
|
|
a9c63e6f2a | ||
|
|
014817810d | ||
|
|
71cf1a8f26 | ||
|
|
a769ca3ae4 | ||
|
|
3d61106cc1 | ||
|
|
6cc114100f | ||
|
|
c11266a96f | ||
|
|
8b0f41e3cb | ||
|
|
1b9ea478da | ||
|
|
8cbda5f5cc | ||
|
|
2f5537c8db | ||
|
|
2327b21c85 | ||
|
|
6652cca642 | ||
|
|
575f37124c | ||
|
|
50a72a16bc | ||
|
|
98513c65f0 | ||
|
|
b61d2212f6 | ||
|
|
1ed957978a | ||
|
|
c00cbf6cb2 | ||
|
|
632f8705e5 | ||
|
|
f8548225e1 | ||
|
|
7fe6b47aa0 | ||
|
|
43f24ad0f1 | ||
|
|
a798b32f25 | ||
|
|
4d1c3f9e52 | ||
|
|
879d2271b7 | ||
|
|
cf4c896431 | ||
|
|
f6363389d0 | ||
|
|
03e8f2d67d | ||
|
|
8e855e53bf | ||
|
|
599636d56b | ||
|
|
9b9534c1eb | ||
|
|
0197ae37c8 | ||
|
|
cf969dfcd6 | ||
|
|
a66be21523 | ||
|
|
4016754d71 | ||
|
|
f3aebf6374 | ||
|
|
f87460b00e | ||
|
|
354e8d7496 | ||
|
|
dc107f8b96 | ||
|
|
cd0f1e45ba | ||
|
|
33022843f2 | ||
|
|
facdf3264a | ||
|
|
4ffb447c53 | ||
|
|
3e5ae613f5 | ||
|
|
42bef4ae1e | ||
|
|
e93e545b81 | ||
|
|
4a8d787f31 | ||
|
|
f5211f6a08 | ||
|
|
57222d2500 | ||
|
|
f479e0d325 | ||
|
|
5302addda0 | ||
|
|
80b017f224 | ||
|
|
b18d582004 | ||
|
|
109394c65b | ||
|
|
c355153f26 | ||
|
|
b87a02beb3 | ||
|
|
4624ffb116 | ||
|
|
a9ce97fb1b | ||
|
|
72ce6cadeb | ||
|
|
c4ff2918a2 | ||
|
|
9972eb3de6 | ||
|
|
ebe0203415 | ||
|
|
b3ef91fe8e | ||
|
|
f7ea1f8dbb | ||
|
|
cf19035b0b | ||
|
|
d9a3f74cb7 |
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -142,7 +142,7 @@ jobs:
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
xvfb-run npm run test:e2e
|
||||
- uses: actions/upload-artifact@v5
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -53,6 +53,7 @@ bruno.iml
|
||||
/blob-report/
|
||||
|
||||
# Development plan files
|
||||
CLAUDE.md
|
||||
*.plan.md
|
||||
|
||||
# packages dist
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
- Use 2 spaces for indentation. No tabs, just spaces – keeps everything neat and uniform.
|
||||
|
||||
- Stick to single quotes for strings. Double quotes are cool elsewhere, but here we go single.
|
||||
- Stick to single quotes for strings. For JSX/TSX attributes, use double quotes (e.g., <svg xmlns="..." viewBox="...">) to follow React conventions.
|
||||
|
||||
- Always add semicolons at the end of statements. It's like putting a period at the end of a sentence – clarity matters.
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
'**/dist/**/*',
|
||||
'**/*.bru',
|
||||
'packages/bruno-js/src/sandbox/bundle-browser-rollup.js',
|
||||
'packages/bruno-app/public/static/**/*'
|
||||
'packages/bruno-app/public/static/**/*',
|
||||
'packages/bruno-app/.next/**/*',
|
||||
'packages/bruno-electron/web/**/*'
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -115,6 +117,18 @@ module.exports = runESMImports().then(() => defineConfig([
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
// Storybook config files use CommonJS with __dirname and module.exports
|
||||
files: ['packages/bruno-app/storybook/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-cli/**/*.js'],
|
||||
ignores: ['**/*.config.js'],
|
||||
|
||||
4616
package-lock.json
generated
4616
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -23,8 +23,13 @@
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@opencollection/types": "~0.7.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
|
||||
"@storybook/builder-webpack5": "^10.1.10",
|
||||
"@storybook/react": "^10.1.10",
|
||||
"@storybook/react-webpack5": "^10.1.10",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
@@ -43,12 +48,13 @@
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"storybook": "^10.1.10",
|
||||
"ts-jest": "^29.2.6"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "node ./scripts/setup.js",
|
||||
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
|
||||
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
|
||||
"dev": "node ./scripts/dev.js",
|
||||
"watch": "npm run dev:watch",
|
||||
"dev:watch": "node ./scripts/dev-hot-reload.js",
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
@@ -56,6 +62,7 @@
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
|
||||
"storybook": "npm run storybook --workspace=packages/bruno-app",
|
||||
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
|
||||
"build:bruno-requests": "npm run build --workspace=packages/bruno-requests",
|
||||
"build:bruno-filestore": "npm run build --workspace=packages/bruno-filestore",
|
||||
@@ -91,5 +98,8 @@
|
||||
"json-schema-typed": "8.0.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.17.1"
|
||||
}
|
||||
}
|
||||
|
||||
4
packages/bruno-app/.gitignore
vendored
4
packages/bruno-app/.gitignore
vendored
@@ -22,6 +22,7 @@ build
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*.log
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
@@ -33,4 +34,5 @@ yarn-error.log*
|
||||
.next/
|
||||
dist/
|
||||
|
||||
.env
|
||||
.env
|
||||
storybook-static/
|
||||
@@ -9,7 +9,9 @@
|
||||
"preview": "rsbuild preview",
|
||||
"test": "jest",
|
||||
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||
"storybook": "storybook dev -p 6006 --config-dir storybook",
|
||||
"build-storybook": "storybook build --config-dir storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
@@ -44,16 +46,19 @@
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsesc": "^3.0.2",
|
||||
"jshint": "^2.13.6",
|
||||
"json5": "^2.2.3",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"jsonschema": "^1.5.0",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.2",
|
||||
"markdown-it-replace-link": "^1.2.0",
|
||||
"mime-types": "^3.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -61,6 +66,7 @@
|
||||
"path": "^0.12.7",
|
||||
"pdfjs-dist": "4.4.168",
|
||||
"platform": "^1.3.6",
|
||||
"polished": "^4.3.1",
|
||||
"posthog-node": "4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"qs": "^6.11.0",
|
||||
@@ -83,9 +89,11 @@
|
||||
"shell-quote": "^1.8.3",
|
||||
"strip-json-comments": "^5.0.1",
|
||||
"styled-components": "^5.3.3",
|
||||
"swagger-ui-react": "5.17.12",
|
||||
"system": "^2.0.1",
|
||||
"url": "^0.11.3",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
const yamlPlugin = (cm) => {
|
||||
cm.defineMode('yaml', function () {
|
||||
var cons = ['true', 'false', 'on', 'off', 'yes', 'no'];
|
||||
var keywordRegex = new RegExp('\\b((' + cons.join(')|(') + '))$', 'i');
|
||||
|
||||
return {
|
||||
token: function (stream, state) {
|
||||
var ch = stream.peek();
|
||||
var esc = state.escaped;
|
||||
state.escaped = false;
|
||||
/* comments */
|
||||
if (ch == '#' && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) {
|
||||
stream.skipToEnd();
|
||||
return 'comment';
|
||||
}
|
||||
|
||||
if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/)) return 'string';
|
||||
|
||||
if (state.literal && stream.indentation() > state.keyCol) {
|
||||
stream.skipToEnd();
|
||||
return 'string';
|
||||
} else if (state.literal) {
|
||||
state.literal = false;
|
||||
}
|
||||
if (stream.sol()) {
|
||||
state.keyCol = 0;
|
||||
state.pair = false;
|
||||
state.pairStart = false;
|
||||
/* document start */
|
||||
if (stream.match('---')) {
|
||||
return 'def';
|
||||
}
|
||||
/* document end */
|
||||
if (stream.match('...')) {
|
||||
return 'def';
|
||||
}
|
||||
/* array list item */
|
||||
if (stream.match(/\s*-\s+/)) {
|
||||
return 'meta';
|
||||
}
|
||||
}
|
||||
/* inline pairs/lists */
|
||||
if (stream.match(/^(\{|\}|\[|\])/)) {
|
||||
if (ch == '{') state.inlinePairs++;
|
||||
else if (ch == '}') state.inlinePairs--;
|
||||
else if (ch == '[') state.inlineList++;
|
||||
else state.inlineList--;
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* list separator */
|
||||
if (state.inlineList > 0 && !esc && ch == ',') {
|
||||
stream.next();
|
||||
return 'meta';
|
||||
}
|
||||
/* pairs separator */
|
||||
if (state.inlinePairs > 0 && !esc && ch == ',') {
|
||||
state.keyCol = 0;
|
||||
state.pair = false;
|
||||
state.pairStart = false;
|
||||
stream.next();
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* start of value of a pair */
|
||||
if (state.pairStart) {
|
||||
/* block literals */
|
||||
if (stream.match(/^\s*(\||\>)\s*/)) {
|
||||
state.literal = true;
|
||||
return 'meta';
|
||||
}
|
||||
/* references */
|
||||
if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) {
|
||||
return 'variable-2';
|
||||
}
|
||||
/* numbers */
|
||||
if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) {
|
||||
return 'number';
|
||||
}
|
||||
if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) {
|
||||
return 'number';
|
||||
}
|
||||
/* keywords */
|
||||
if (stream.match(keywordRegex)) {
|
||||
return 'keyword';
|
||||
}
|
||||
}
|
||||
|
||||
/* pairs (associative arrays) -> key */
|
||||
if (
|
||||
!state.pair
|
||||
&& stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/)
|
||||
) {
|
||||
state.pair = true;
|
||||
state.keyCol = stream.indentation();
|
||||
return 'atom';
|
||||
}
|
||||
if (state.pair && stream.match(/^:\s*/)) {
|
||||
state.pairStart = true;
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* nothing found, continue */
|
||||
state.pairStart = false;
|
||||
state.escaped = ch == '\\';
|
||||
stream.next();
|
||||
return null;
|
||||
},
|
||||
startState: function () {
|
||||
return {
|
||||
pair: false,
|
||||
pairStart: false,
|
||||
keyCol: 0,
|
||||
inlinePairs: 0,
|
||||
inlineList: 0,
|
||||
literal: false,
|
||||
escaped: false
|
||||
};
|
||||
},
|
||||
lineComment: '#',
|
||||
fold: 'indent'
|
||||
};
|
||||
});
|
||||
|
||||
cm.defineMIME('text/x-yaml', 'yaml');
|
||||
cm.defineMIME('text/yaml', 'yaml');
|
||||
};
|
||||
|
||||
export default yamlPlugin;
|
||||
@@ -0,0 +1,65 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
height: calc(100vh - 4rem);
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
font-family: ${(props) => (props.font ? props.font : 'default')};
|
||||
line-break: anywhere;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog {
|
||||
overflow: visible;
|
||||
input {
|
||||
background: transparent;
|
||||
border: 1px solid #d3d6db;
|
||||
outline: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
.CodeMirror-overlayscroll-vertical div {
|
||||
background: #d2d7db;
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Todo: dark mode temporary fix
|
||||
// Clean this
|
||||
.CodeMirror.cm-s-monokai {
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
.CodeMirror-overlayscroll-vertical div {
|
||||
background: #444444;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property,
|
||||
.cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-string {
|
||||
color: #ce9178 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-number {
|
||||
color: #b5cea8 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-atom {
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
|
||||
.cm-variable-valid {
|
||||
color: ${(props) => props.theme.codemirror.variable.valid};
|
||||
}
|
||||
.cm-variable-invalid {
|
||||
color: ${(props) => props.theme.codemirror.variable.invalid};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import yamlPlugin from './Plugins/Yaml/index';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
this.lintOptions = {
|
||||
esversion: 11,
|
||||
expr: true,
|
||||
asi: true
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
switch (this.props.mode) {
|
||||
case 'yaml':
|
||||
// YAML linting and hightlighting plugin
|
||||
yamlPlugin(CodeMirror);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const editor = (this.editor = CodeMirror(this._node, {
|
||||
value: this.props.value || '',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
tabSize: 2,
|
||||
mode: this.props.mode || 'application/text',
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
showCursorWhenSelecting: true,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||
lint: this.lintOptions,
|
||||
readOnly: this.props.readOnly,
|
||||
scrollbarStyle: 'overlay',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
extraKeys: {
|
||||
'Cmd-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Ctrl-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Cmd-F': 'findPersistent',
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
'Tab': function (cm) {
|
||||
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
|
||||
? cm.execCommand('indentMore')
|
||||
: cm.replaceSelection(' ', 'end');
|
||||
},
|
||||
'Shift-Tab': 'indentLess',
|
||||
'Ctrl-Space': 'autocomplete',
|
||||
'Cmd-Space': 'autocomplete',
|
||||
'Ctrl-Y': 'foldAll',
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
'Cmd-I': 'unfoldAll'
|
||||
}
|
||||
}));
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this.ignoreChangeEvent = true;
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
}
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.editor) {
|
||||
this.editor.refresh();
|
||||
}
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full w-full graphiql-container"
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
this.cachedValue = this.editor.getValue();
|
||||
if (this.props.onEdit) {
|
||||
this.props.onEdit(this.cachedValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from './CodeEditor/index';
|
||||
import { IconDeviceFloppy } from '@tabler/icons';
|
||||
import { saveApiSpecToFile } from 'providers/ReduxStore/slices/apiSpec';
|
||||
import { useState } from 'react';
|
||||
|
||||
const FileEditor = ({ apiSpec }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme, theme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const [content, setContent] = useState(apiSpec?.raw);
|
||||
|
||||
const onEdit = (value) => {
|
||||
setContent(value);
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
dispatch(saveApiSpecToFile({ uid: apiSpec?.uid, content }));
|
||||
};
|
||||
|
||||
const hasChanges = Boolean(content != apiSpec?.raw);
|
||||
|
||||
const editorMode = 'yaml';
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow relative">
|
||||
<CodeEditor
|
||||
theme={displayedTheme}
|
||||
value={content}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
mode={editorMode}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
<IconDeviceFloppy
|
||||
onClick={onSave}
|
||||
color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className={`absolute right-0 top-0 m-4 ${
|
||||
hasChanges ? 'cursor-pointer oapcity-100' : 'cursor-default opacity-50'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileEditor;
|
||||
@@ -0,0 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.swagger-root {
|
||||
height: calc(100vh - 4rem);
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
|
||||
&.dark {
|
||||
.swagger-ui {
|
||||
filter: invert(88%) hue-rotate(180deg);
|
||||
}
|
||||
.swagger-ui .microlight {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,19 @@
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const Swagger = ({ string }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
|
||||
console.log('string', string);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className={`swagger-root w-full overflow-y-scroll ${displayedTheme}`}>
|
||||
<SwaggerUI spec={string} />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Swagger;
|
||||
@@ -0,0 +1,22 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.menu-icon {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
}
|
||||
|
||||
div.dropdown-item.menu-item {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.colors.bg.danger};
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.react-tooltip {
|
||||
z-index: 10;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
97
packages/bruno-app/src/components/ApiSpecPanel/index.js
Normal file
97
packages/bruno-app/src/components/ApiSpecPanel/index.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { forwardRef, useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconFileCode, IconDots } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FileEditor from './FileEditor';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { openApiSpec } from 'providers/ReduxStore/slices/apiSpec';
|
||||
import { useState } from 'react';
|
||||
import CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec';
|
||||
import { Suspense } from 'react';
|
||||
import Swagger from './Renderers/Swagger';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const ApiSpecPanel = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [createApiSpecModalOpen, setCreateApiSpecModalOpen] = useState(false);
|
||||
|
||||
const { apiSpecs, activeApiSpecUid } = useSelector((state) => state.apiSpec);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
let apiSpec = find(apiSpecs, (c) => c.uid === activeApiSpecUid);
|
||||
const { filename, pathname, raw, uid } = apiSpec || {};
|
||||
if (!uid) {
|
||||
return <div className="p-4 opacity-50">API Spec not found!</div>;
|
||||
}
|
||||
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<IconDots size={22} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleOpenApiSpec = () => {
|
||||
dispatch(openApiSpec()).catch(
|
||||
(err) => console.log(err) && toast.error('An error occurred while opening the API spec')
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col flex-grow relative">
|
||||
{createApiSpecModalOpen ? <CreateApiSpec onClose={() => setCreateApiSpecModalOpen(false)} /> : null}
|
||||
<div className="p-3 mb-2 w-full flex flex-row justify-between grid grid-cols-3">
|
||||
<div className="flex flex-row justify-start gap-x-4 col-span-1">
|
||||
<div className="flex w-fit items-center cursor-pointer">
|
||||
<IconFileCode size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2 mr-4 font-semibold">API Designer</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full col-span-1 flex justify-center" title={pathname}>
|
||||
{filename}
|
||||
</div>
|
||||
<div className="menu-icon pr-2 col-span-1 flex justify-end">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setCreateApiSpecModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Create API Spec
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleOpenApiSpec();
|
||||
}}
|
||||
>
|
||||
Open API Spec
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<section className="main flex flex-grow px-4 relative">
|
||||
<div className="w-full grid grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<FileEditor apiSpec={apiSpec} />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Suspense fallback="">
|
||||
<Swagger string={raw} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiSpecPanel;
|
||||
251
packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js
Normal file
251
packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js
Normal file
@@ -0,0 +1,251 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
|
||||
.titlebar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
padding-left: 70px; /* Space for macOS window controls */
|
||||
transition: padding-left 0.15s ease;
|
||||
}
|
||||
|
||||
/* When in full screen, no traffic lights so reduce padding */
|
||||
&.fullscreen .titlebar-content {
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
/* Remove drag region from interactive elements */
|
||||
.workspace-name-container,
|
||||
.dropdown-item,
|
||||
.home-button,
|
||||
.dropdown,
|
||||
button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* Left section */
|
||||
.titlebar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: 10px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* When in full screen, no traffic lights so remove margin-left */
|
||||
&.fullscreen .titlebar-left {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
/* Workspace Name Dropdown Trigger */
|
||||
.workspace-name-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
flex-shrink: 0;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* Center section - Bruno branding */
|
||||
.titlebar-center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
pointer-events: none;
|
||||
|
||||
.bruno-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.text};
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Right section */
|
||||
.titlebar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* App action buttons container */
|
||||
.titlebar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Workspace Dropdown Styles */
|
||||
.workspace-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 10px !important;
|
||||
margin: 0 !important;
|
||||
|
||||
&.active {
|
||||
.check-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.pin-btn:not(.pinned) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.workspace-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: ${(props) => props.theme.workspace?.accent || props.theme.colors?.text?.yellow};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pin-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
|
||||
opacity: 0;
|
||||
|
||||
&.pinned {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.dropdown.hoverBg};
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Adjust for non-macOS platforms */
|
||||
&:not(.os-mac) .titlebar-content {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/* Windows-specific styles */
|
||||
&.os-windows .titlebar-content {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&.os-windows .titlebar-left {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
&.os-linux .titlebar-content {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&.os-linux .titlebar-left {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* Custom window control buttons for Windows - always interactive, above modal overlay */
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 36px;
|
||||
margin-left: 8px;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.window-control-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 46px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
&.close:hover {
|
||||
background: #e81123;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
333
packages/bruno-app/src/components/AppTitleBar/index.js
Normal file
333
packages/bruno-app/src/components/AppTitleBar/index.js
Normal file
@@ -0,0 +1,333 @@
|
||||
import React from 'react';
|
||||
import { IconCheck, IconChevronDown, IconFolder, IconHome, IconPin, IconPinned, IconPlus, IconDownload, IconSettings, IconMinus, IconSquare, IconX, IconCopy } from '@tabler/icons';
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { savePreferences, showHomePage, showManageWorkspacePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs';
|
||||
import { openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces';
|
||||
|
||||
import Bruno from 'components/Bruno';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
|
||||
import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';
|
||||
import ImportWorkspace from 'components/WorkspaceSidebar/ImportWorkspace';
|
||||
|
||||
import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { toTitleCase } from 'utils/common/index';
|
||||
import ResponseLayoutToggle from 'components/ResponsePane/ResponseLayoutToggle';
|
||||
import { isMacOS, isWindowsOS, isLinuxOS } from 'utils/common/platform';
|
||||
|
||||
const getOsClass = () => {
|
||||
if (isMacOS()) return 'os-mac';
|
||||
if (isWindowsOS()) return 'os-windows';
|
||||
if (isLinuxOS()) return 'os-linux';
|
||||
return 'os-other';
|
||||
};
|
||||
|
||||
const AppTitleBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
const osClass = getOsClass();
|
||||
const isWindows = osClass === 'os-windows';
|
||||
const isLinux = osClass === 'os-linux';
|
||||
const showWindowControls = isWindows || isLinux;
|
||||
|
||||
// Listen for fullscreen changes
|
||||
useEffect(() => {
|
||||
const { ipcRenderer } = window;
|
||||
if (!ipcRenderer) return;
|
||||
|
||||
const removeEnterFullScreenListener = ipcRenderer.on('main:enter-full-screen', () => {
|
||||
setIsFullScreen(true);
|
||||
});
|
||||
|
||||
const removeLeaveFullScreenListener = ipcRenderer.on('main:leave-full-screen', () => {
|
||||
setIsFullScreen(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeEnterFullScreenListener();
|
||||
removeLeaveFullScreenListener();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showWindowControls) return;
|
||||
const { ipcRenderer } = window;
|
||||
if (!ipcRenderer) return;
|
||||
|
||||
ipcRenderer.invoke('renderer:window-is-maximized')
|
||||
.then((maximized) => {
|
||||
setIsMaximized(maximized);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error getting initial maximized state:', error);
|
||||
});
|
||||
|
||||
const removeMaximizedListener = ipcRenderer.on('main:window-maximized', () => {
|
||||
setIsMaximized(true);
|
||||
});
|
||||
|
||||
const removeUnmaximizedListener = ipcRenderer.on('main:window-unmaximized', () => {
|
||||
setIsMaximized(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeMaximizedListener();
|
||||
removeUnmaximizedListener();
|
||||
};
|
||||
}, [showWindowControls]);
|
||||
|
||||
const handleMinimize = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-minimize');
|
||||
}, []);
|
||||
|
||||
const handleMaximize = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-maximize');
|
||||
// State will be updated via IPC events from main process (main:window-maximized/main:window-unmaximized)
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-close');
|
||||
}, []);
|
||||
|
||||
// Get workspace info
|
||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen);
|
||||
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
|
||||
|
||||
// Sort workspaces according to preferences
|
||||
const sortedWorkspaces = useMemo(() => {
|
||||
return sortWorkspaces(workspaces, preferences);
|
||||
}, [workspaces, preferences]);
|
||||
|
||||
const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);
|
||||
const [importWorkspaceModalOpen, setImportWorkspaceModalOpen] = useState(false);
|
||||
|
||||
const WorkspaceName = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="workspace-name-container" {...props}>
|
||||
<span className="workspace-name">{toTitleCase(activeWorkspace?.name) || 'Default Workspace'}</span>
|
||||
<IconChevronDown size={14} stroke={1.5} className="chevron-icon" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleHomeClick = () => {
|
||||
dispatch(showHomePage());
|
||||
};
|
||||
|
||||
const handleWorkspaceSwitch = (workspaceUid) => {
|
||||
dispatch(switchWorkspace(workspaceUid));
|
||||
toast.success(`Switched to ${workspaces.find((w) => w.uid === workspaceUid)?.name}`);
|
||||
};
|
||||
|
||||
const handleOpenWorkspace = async () => {
|
||||
try {
|
||||
await dispatch(openWorkspaceDialog());
|
||||
toast.success('Workspace opened successfully');
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'Failed to open workspace');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateWorkspace = () => {
|
||||
setCreateWorkspaceModalOpen(true);
|
||||
};
|
||||
|
||||
const handleManageWorkspaces = () => {
|
||||
dispatch(showManageWorkspacePage());
|
||||
};
|
||||
|
||||
const handleImportWorkspace = () => {
|
||||
setImportWorkspaceModalOpen(true);
|
||||
};
|
||||
|
||||
const handlePinWorkspace = useCallback((workspaceUid, e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const newPreferences = toggleWorkspacePin(workspaceUid, preferences);
|
||||
dispatch(savePreferences(newPreferences));
|
||||
}, [dispatch, preferences]);
|
||||
|
||||
const handleToggleSidebar = () => {
|
||||
dispatch(toggleSidebarCollapse());
|
||||
};
|
||||
|
||||
const handleToggleDevtools = () => {
|
||||
if (isConsoleOpen) {
|
||||
dispatch(closeConsole());
|
||||
} else {
|
||||
dispatch(openConsole());
|
||||
}
|
||||
};
|
||||
|
||||
// Build workspace menu items
|
||||
const workspaceMenuItems = useMemo(() => {
|
||||
const items = sortedWorkspaces.map((workspace) => {
|
||||
const isActive = workspace.uid === activeWorkspaceUid;
|
||||
const isPinned = preferences?.workspaces?.pinnedWorkspaceUids?.includes(workspace.uid);
|
||||
|
||||
return {
|
||||
id: workspace.uid,
|
||||
label: toTitleCase(workspace.name),
|
||||
onClick: () => handleWorkspaceSwitch(workspace.uid),
|
||||
className: `workspace-item ${isActive ? 'active' : ''}`,
|
||||
rightSection: (
|
||||
<div className="workspace-actions">
|
||||
{workspace.type !== 'default' && (
|
||||
<ActionIcon
|
||||
className={`pin-btn ${isPinned ? 'pinned' : ''}`}
|
||||
onClick={(e) => handlePinWorkspace(workspace.uid, e)}
|
||||
label={isPinned ? 'Unpin workspace' : 'Pin workspace'}
|
||||
size="sm"
|
||||
>
|
||||
{isPinned ? (
|
||||
<IconPinned size={14} stroke={1.5} />
|
||||
) : (
|
||||
<IconPin size={14} stroke={1.5} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
)}
|
||||
{isActive && <IconCheck size={16} stroke={1.5} className="check-icon" />}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
// Add label and action items
|
||||
items.push(
|
||||
{ type: 'label', label: 'Workspaces' },
|
||||
{
|
||||
id: 'create-workspace',
|
||||
leftSection: IconPlus,
|
||||
label: 'Create workspace',
|
||||
onClick: handleCreateWorkspace
|
||||
},
|
||||
{
|
||||
id: 'open-workspace',
|
||||
leftSection: IconFolder,
|
||||
label: 'Open workspace',
|
||||
onClick: handleOpenWorkspace
|
||||
},
|
||||
{
|
||||
id: 'import-workspace',
|
||||
leftSection: IconDownload,
|
||||
label: 'Import workspace',
|
||||
onClick: handleImportWorkspace
|
||||
},
|
||||
{
|
||||
id: 'manage-workspaces',
|
||||
leftSection: IconSettings,
|
||||
label: 'Manage workspaces',
|
||||
onClick: handleManageWorkspaces
|
||||
}
|
||||
);
|
||||
|
||||
return items;
|
||||
}, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`app-titlebar ${osClass} ${isFullScreen ? 'fullscreen' : ''}`}>
|
||||
{createWorkspaceModalOpen && (
|
||||
<CreateWorkspace onClose={() => setCreateWorkspaceModalOpen(false)} />
|
||||
)}
|
||||
{importWorkspaceModalOpen && (
|
||||
<ImportWorkspace onClose={() => setImportWorkspaceModalOpen(false)} />
|
||||
)}
|
||||
|
||||
<div className="titlebar-content">
|
||||
{/* Left section: Home + Workspace */}
|
||||
<div className="titlebar-left">
|
||||
<ActionIcon
|
||||
onClick={handleHomeClick}
|
||||
label="Home"
|
||||
size="lg"
|
||||
className="home-button"
|
||||
>
|
||||
<IconHome size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
{/* Workspace Dropdown */}
|
||||
<MenuDropdown
|
||||
data-testid="workspace-menu"
|
||||
items={workspaceMenuItems}
|
||||
placement="bottom-start"
|
||||
selectedItemId={activeWorkspaceUid}
|
||||
>
|
||||
<WorkspaceName />
|
||||
</MenuDropdown>
|
||||
</div>
|
||||
|
||||
{/* Center section: Bruno logo + text */}
|
||||
<div className="titlebar-center">
|
||||
<Bruno width={18} />
|
||||
<span className="bruno-text">Bruno</span>
|
||||
</div>
|
||||
|
||||
{/* Right section: Action buttons */}
|
||||
<div className="titlebar-right">
|
||||
<div className="titlebar-actions">
|
||||
{/* Toggle sidebar */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleSidebar}
|
||||
label={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
|
||||
size="lg"
|
||||
data-testid="toggle-sidebar-button"
|
||||
>
|
||||
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
{/* Toggle devtools */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleDevtools}
|
||||
label={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}
|
||||
size="lg"
|
||||
data-testid="toggle-devtools-button"
|
||||
>
|
||||
<IconBottombarToggle collapsed={!isConsoleOpen} size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
<ResponseLayoutToggle />
|
||||
</div>
|
||||
|
||||
{showWindowControls && (
|
||||
<div className="window-controls">
|
||||
<button
|
||||
className="window-control-btn minimize"
|
||||
onClick={handleMinimize}
|
||||
aria-label="Minimize"
|
||||
>
|
||||
<IconMinus size={16} stroke={1} />
|
||||
</button>
|
||||
<button
|
||||
className="window-control-btn maximize"
|
||||
onClick={handleMaximize}
|
||||
aria-label={isMaximized ? 'Restore' : 'Maximize'}
|
||||
>
|
||||
{isMaximized ? <IconCopy size={14} stroke={1} /> : <IconSquare size={14} stroke={1} />}
|
||||
</button>
|
||||
<button
|
||||
className="window-control-btn close"
|
||||
onClick={handleClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
<IconX size={16} stroke={1} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppTitleBar;
|
||||
@@ -0,0 +1,38 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
|
||||
.body-mode-selector {
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
padding-left: 1.5rem !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
fill: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,17 +1,33 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import React, { useMemo } from 'react';
|
||||
import { IconCaretDown, IconForms, IconBraces, IconCode, IconFileText, IconDatabase, IconFile, IconX } from '@tabler/icons';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import { humanizeRequestBodyMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DEFAULT_MODES = [
|
||||
{ key: 'multipartForm', label: 'Multipart Form', category: 'Form' },
|
||||
{ key: 'formUrlEncoded', label: 'Form URL Encoded', category: 'Form' },
|
||||
{ key: 'json', label: 'JSON', category: 'Raw' },
|
||||
{ key: 'xml', label: 'XML', category: 'Raw' },
|
||||
{ key: 'text', label: 'TEXT', category: 'Raw' },
|
||||
{ key: 'sparql', label: 'SPARQL', category: 'Raw' },
|
||||
{ key: 'file', label: 'File / Binary', category: 'Other' },
|
||||
{ key: 'none', label: 'None', category: 'Other' }
|
||||
{
|
||||
name: 'Form',
|
||||
options: [
|
||||
{ id: 'multipartForm', label: 'Multipart Form', leftSection: IconForms },
|
||||
{ id: 'formUrlEncoded', label: 'Form URL Encoded', leftSection: IconForms }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Raw',
|
||||
options: [
|
||||
{ id: 'json', label: 'JSON', leftSection: IconBraces },
|
||||
{ id: 'xml', label: 'XML', leftSection: IconCode },
|
||||
{ id: 'text', label: 'TEXT', leftSection: IconFileText },
|
||||
{ id: 'sparql', label: 'SPARQL', leftSection: IconDatabase }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Other',
|
||||
options: [
|
||||
{ id: 'file', label: 'File / Binary', leftSection: IconFile },
|
||||
{ id: 'none', label: 'No Body', leftSection: IconX }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const BodyModeSelector = ({
|
||||
@@ -21,62 +37,39 @@ const BodyModeSelector = ({
|
||||
disabled = false,
|
||||
className = '',
|
||||
wrapperClassName = '',
|
||||
showCategories = true,
|
||||
placement = 'bottom-end'
|
||||
}) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
|
||||
{humanizeRequestBodyMode(currentMode)}
|
||||
{' '}
|
||||
<IconCaretDown className="caret ml-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeSelect = (mode) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange(mode);
|
||||
};
|
||||
|
||||
// Group modes by category for rendering
|
||||
const groupedModes = modes.reduce((acc, mode) => {
|
||||
const category = mode.category || 'Other';
|
||||
if (!acc[category]) {
|
||||
acc[category] = [];
|
||||
}
|
||||
acc[category].push(mode);
|
||||
return acc;
|
||||
}, {});
|
||||
// Add onClick handlers to mode options
|
||||
const menuItems = useMemo(() => {
|
||||
return modes.map((group) => ({
|
||||
...group,
|
||||
options: group.options.map((option) => ({
|
||||
...option,
|
||||
onClick: () => onModeChange(option.id)
|
||||
}))
|
||||
}));
|
||||
}, [modes, onModeChange]);
|
||||
|
||||
return (
|
||||
<div className={`inline-flex items-center body-mode-selector ${disabled ? 'cursor-default' : 'cursor-pointer'} ${wrapperClassName}`}>
|
||||
<Dropdown
|
||||
onCreate={onDropdownCreate}
|
||||
icon={<Icon />}
|
||||
placement={placement}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
>
|
||||
{Object.entries(groupedModes).map(([category, categoryModes]) => (
|
||||
<React.Fragment key={category}>
|
||||
{showCategories && <div className="label-item font-medium">{category}</div>}
|
||||
{categoryModes.map((mode) => (
|
||||
<div
|
||||
key={mode.key}
|
||||
className="dropdown-item"
|
||||
onClick={() => onModeSelect(mode.key)}
|
||||
>
|
||||
{mode.label}
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
<StyledWrapper className={wrapperClassName}>
|
||||
<div className={`inline-flex items-center body-mode-selector ${disabled ? 'cursor-default' : 'cursor-pointer'}`}>
|
||||
<MenuDropdown
|
||||
items={menuItems}
|
||||
placement={placement}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
selectedItemId={currentMode}
|
||||
showGroupDividers={false}
|
||||
groupStyle="select"
|
||||
>
|
||||
<div className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
|
||||
{humanizeRequestBodyMode(currentMode)}
|
||||
{' '}
|
||||
<IconCaretDown className="caret ml-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
</MenuDropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -21,7 +22,8 @@ const BulkEditor = ({ params, onChange, onToggle, onSave, onRun }) => {
|
||||
<CodeEditor
|
||||
mode="text/plain"
|
||||
theme={displayedTheme}
|
||||
font={preferences.codeFont || 'default'}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={parsedParams}
|
||||
onEdit={handleEdit}
|
||||
onSave={onSave}
|
||||
|
||||
@@ -36,12 +36,12 @@ const StyledWrapper = styled.div`
|
||||
|
||||
/* Style line numbers when there's a lint issue */
|
||||
.CodeMirror-lint-line-error .CodeMirror-linenumber {
|
||||
color: #d32f2f !important;
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-line-warning .CodeMirror-linenumber {
|
||||
color: #f57c00 !important;
|
||||
color: ${(props) => props.theme.colors.text.warning} !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ const StyledWrapper = styled.div`
|
||||
span.cm-atom {
|
||||
color: ${(props) => props.theme.codemirror.tokens.atom} !important;
|
||||
}
|
||||
span.cm-variable {
|
||||
span.cm-variable, span.cm-variable-2 {
|
||||
color: ${(props) => props.theme.codemirror.tokens.variable} !important;
|
||||
}
|
||||
span.cm-keyword {
|
||||
@@ -128,14 +128,20 @@ const StyledWrapper = styled.div`
|
||||
span.cm-operator {
|
||||
color: ${(props) => props.theme.codemirror.tokens.operator} !important;
|
||||
}
|
||||
span.cm-tag {
|
||||
color: ${(props) => props.theme.codemirror.tokens.tag} !important;
|
||||
}
|
||||
span.cm-tag.cm-bracket {
|
||||
color: ${(props) => props.theme.codemirror.tokens.tagBracket} !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Variable validation colors */
|
||||
.cm-variable-valid {
|
||||
color: #5fad89 !important; /* Soft sage */
|
||||
color: ${(props) => props.theme.codemirror.variable.valid} !important;
|
||||
}
|
||||
.cm-variable-invalid {
|
||||
color: #d17b7b !important; /* Soft coral */
|
||||
color: ${(props) => props.theme.codemirror.variable.invalid} !important;
|
||||
}
|
||||
|
||||
.CodeMirror-search-hint {
|
||||
|
||||
@@ -192,7 +192,6 @@ export default class CodeEditor extends React.Component {
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
editor.on('scroll', this.onScroll);
|
||||
editor.scrollTo(null, this.props.initialScroll);
|
||||
this.addOverlay();
|
||||
|
||||
@@ -229,8 +228,10 @@ export default class CodeEditor extends React.Component {
|
||||
CodeMirror.signal(this.editor, 'change', this.editor);
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
const cursor = this.editor.getCursor();
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
this.editor.setCursor(cursor);
|
||||
}
|
||||
|
||||
if (this.editor) {
|
||||
@@ -275,13 +276,19 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll(this.editor);
|
||||
}
|
||||
|
||||
this.editor?._destroyLinkAware?.();
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor.off('scroll', this.onScroll);
|
||||
|
||||
// Clean up lint error tooltip
|
||||
this.cleanupLintErrorTooltip?.();
|
||||
|
||||
const wrapper = this.editor.getWrapperElement();
|
||||
wrapper?.parentNode?.removeChild(wrapper);
|
||||
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
@@ -325,8 +332,6 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
onScroll = (event) => this.props.onScroll?.(event);
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.bruno-search-bar {
|
||||
@@ -9,15 +10,15 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0 2px;
|
||||
min-height: 36px;
|
||||
background: ${(props) => props.theme.sidebar.search.bg} !important;
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${(props) => props.theme.sidebar.search.bg} !important;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
gap: 0;
|
||||
padding: 1px 3px;
|
||||
width: auto;
|
||||
min-width: 180px;
|
||||
max-width: 320px;
|
||||
min-height: 22px;
|
||||
background: ${(props) => props.theme.background.base};
|
||||
color: ${(props) => props.theme.text.base};
|
||||
border: solid 1px ${(props) => props.theme.border.border2};
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
}
|
||||
|
||||
.bruno-search-bar input {
|
||||
@@ -38,7 +39,7 @@ const StyledWrapper = styled.div`
|
||||
padding: 0 1px;
|
||||
margin: 0 1px;
|
||||
cursor: pointer;
|
||||
color: #aaa;
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
border-radius: 3px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
@@ -51,27 +52,14 @@ const StyledWrapper = styled.div`
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
color: #aaa;
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
margin: 0 8px 0 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bruno-search-bar.compact {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
color: ${(props) => props.theme.codemirror.text || props.theme.text};
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
border-radius: 4px;
|
||||
padding: 1px 3px;
|
||||
min-height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.bruno-search-bar input {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
color: ${(props) => props.theme.colors.text.subtext2};
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
@@ -92,7 +80,9 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.searchbar-icon-btn.active {
|
||||
color: #f39c12 !important;
|
||||
color: ${(props) => props.theme.brand};
|
||||
background-color: ${(props) => rgba(props.theme.brand, 0.1)};
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ const CodeMirrorSearch = ({ visible, editor, onClose }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="bruno-search-bar compact">
|
||||
<div className="bruno-search-bar">
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
@@ -13,7 +14,8 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.auth-placement-selector {
|
||||
padding: 0.5rem 0px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
padding: 0.2rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
@@ -37,7 +39,6 @@ const Wrapper = styled.div`
|
||||
|
||||
.auth-type-label {
|
||||
width: fit-content;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
@@ -57,29 +57,31 @@ const ApiKeyAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Key</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={apikeyAuth.key || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAuthChange('key', val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Value</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Value</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={apikeyAuth.value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAuthChange('value', val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Add To</label>
|
||||
<label className="block mb-1">Add To</label>
|
||||
<div className="inline-flex items-center cursor-pointer auth-placement-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
|
||||
@@ -7,21 +7,13 @@ const Wrapper = styled.div`
|
||||
background: transparent;
|
||||
|
||||
.auth-mode-label {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140, 140, 140);
|
||||
}
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
@@ -9,113 +9,77 @@ import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AuthMode = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeChange = (value) => {
|
||||
const onModeChange = useCallback((value) => {
|
||||
dispatch(
|
||||
updateCollectionAuthMode({
|
||||
collectionUid: collection.uid,
|
||||
mode: value
|
||||
})
|
||||
);
|
||||
};
|
||||
}, [dispatch, collection.uid]);
|
||||
|
||||
const menuItems = useMemo(() => [
|
||||
{
|
||||
id: 'awsv4',
|
||||
label: 'AWS Sig v4',
|
||||
onClick: () => onModeChange('awsv4')
|
||||
},
|
||||
{
|
||||
id: 'basic',
|
||||
label: 'Basic Auth',
|
||||
onClick: () => onModeChange('basic')
|
||||
},
|
||||
{
|
||||
id: 'wsse',
|
||||
label: 'WSSE Auth',
|
||||
onClick: () => onModeChange('wsse')
|
||||
},
|
||||
{
|
||||
id: 'bearer',
|
||||
label: 'Bearer Token',
|
||||
onClick: () => onModeChange('bearer')
|
||||
},
|
||||
{
|
||||
id: 'digest',
|
||||
label: 'Digest Auth',
|
||||
onClick: () => onModeChange('digest')
|
||||
},
|
||||
{
|
||||
id: 'ntlm',
|
||||
label: 'NTLM Auth',
|
||||
onClick: () => onModeChange('ntlm')
|
||||
},
|
||||
{
|
||||
id: 'oauth2',
|
||||
label: 'OAuth 2.0',
|
||||
onClick: () => onModeChange('oauth2')
|
||||
},
|
||||
{
|
||||
id: 'apikey',
|
||||
label: 'API Key',
|
||||
onClick: () => onModeChange('apikey')
|
||||
},
|
||||
{
|
||||
id: 'none',
|
||||
label: 'No Auth',
|
||||
onClick: () => onModeChange('none')
|
||||
}
|
||||
], [onModeChange]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('awsv4');
|
||||
}}
|
||||
>
|
||||
AWS Sig v4
|
||||
<MenuDropdown
|
||||
items={menuItems}
|
||||
placement="bottom-end"
|
||||
selectedItemId={authMode}
|
||||
>
|
||||
<div className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('basic');
|
||||
}}
|
||||
>
|
||||
Basic Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('wsse');
|
||||
}}
|
||||
>
|
||||
WSSE Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('bearer');
|
||||
}}
|
||||
>
|
||||
Bearer Token
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('digest');
|
||||
}}
|
||||
>
|
||||
Digest Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('ntlm');
|
||||
}}
|
||||
>
|
||||
NTLM Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('apikey');
|
||||
}}
|
||||
>
|
||||
API Key
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Auth
|
||||
</div>
|
||||
</Dropdown>
|
||||
</MenuDropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -123,19 +123,20 @@ const AwsV4Auth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Access Key ID</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Access Key ID</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.accessKeyId || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAccessKeyIdChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2 flex items-center">
|
||||
<label className="block mb-1">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-3 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
@@ -143,51 +144,56 @@ const AwsV4Auth = ({ collection }) => {
|
||||
onChange={(val) => handleSecretAccessKeyChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="awsv4-secret-access-key" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Session Token</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Session Token</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.sessionToken || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleSessionTokenChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Service</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Service</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.service || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleServiceChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Region</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Region</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.region || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleRegionChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Profile Name</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Profile Name</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.profileName || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleProfileNameChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -47,18 +47,19 @@ const BasicAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<label className="block mb-1">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
@@ -67,6 +68,7 @@ const BasicAuth = ({ collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -33,7 +33,7 @@ const BearerAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Token</label>
|
||||
<label className="block mb-1">Token</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
@@ -42,6 +42,7 @@ const BearerAuth = ({ collection }) => {
|
||||
onChange={(val) => handleTokenChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="bearer-token" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -47,18 +47,19 @@ const DigestAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<label className="block mb-1">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.password || ''}
|
||||
@@ -67,6 +68,7 @@ const DigestAuth = ({ collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="digest-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -64,19 +64,20 @@ const NTLMAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<label className="block mb-1">Password</label>
|
||||
<div className="single-line-editor-wrapper mb-3 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -84,11 +85,12 @@ const NTLMAuth = ({ collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="ntlm-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Domain</label>
|
||||
<label className="block mb-1">Domain</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.domain || ''}
|
||||
@@ -96,6 +98,7 @@ const NTLMAuth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleDomainChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -47,18 +47,19 @@ const WsseAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUserChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<label className="block mb-1">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.password || ''}
|
||||
@@ -67,6 +68,7 @@ const WsseAuth = ({ collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import OAuth2 from './OAuth2';
|
||||
import NTLMAuth from './NTLMAuth';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Auth = ({ collection }) => {
|
||||
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
|
||||
@@ -59,9 +60,9 @@ const Auth = ({ collection }) => {
|
||||
</div>
|
||||
{getAuthView()}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -30,11 +30,11 @@ const StyledWrapper = styled.div`
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
border: solid 1px ${(props) => props.theme.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
@@ -48,32 +48,37 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.protocol-https,
|
||||
.protocol-grpcs {
|
||||
.protocol-grpcs,
|
||||
.protocol-wss {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.protocol-https {
|
||||
animation: slideUpDown 6s infinite;
|
||||
animation: slideUpDown 9s infinite;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.protocol-grpcs {
|
||||
animation: slideUpDown 6s infinite 3s;
|
||||
animation: slideUpDown 9s infinite 3s;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.protocol-wss {
|
||||
animation: slideUpDown 9s infinite 6s;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
@keyframes slideUpDown {
|
||||
0%, 45% {
|
||||
0%, 30% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50%, 95% {
|
||||
33.33%, 97% {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
100% {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useDispatch } from 'react-redux';
|
||||
import { updateCollectionClientCertificates } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import get from 'lodash/get';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ClientCertSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -180,6 +181,7 @@ const ClientCertSettings = ({ collection }) => {
|
||||
<span className="protocol-placeholder">
|
||||
<span className="protocol-https">https://</span>
|
||||
<span className="protocol-grpcs">grpcs://</span>
|
||||
<span className="protocol-wss">wss://</span>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
@@ -373,13 +375,13 @@ const ClientCertSettings = ({ collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6 flex flex-row gap-2 items-center">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
<Button type="submit" size="sm" data-testid="add-client-cert">
|
||||
Add
|
||||
</button>
|
||||
</Button>
|
||||
<div className="h-4 border-l border-gray-600"></div>
|
||||
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -9,6 +9,8 @@ import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconEdit, IconX, IconFileText } from '@tabler/icons';
|
||||
import Button from 'ui/Button/index';
|
||||
import ActionIcon from 'ui/ActionIcon/index';
|
||||
|
||||
const Docs = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -55,17 +57,17 @@ const Docs = ({ collection }) => {
|
||||
<div className="flex flex-row gap-2 items-center justify-center">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
|
||||
<IconX className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={onSave}>
|
||||
<Button type="button" color="secondary" onClick={handleDiscardChanges}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" onClick={onSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
<ActionIcon className="editing-mode" onClick={toggleViewMode}>
|
||||
<IconEdit className="cursor-pointer" size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
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 {
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader,
|
||||
setCollectionHeaders
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { setCollectionHeaders } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import EditableTable from 'components/EditableTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
import BulkEditor from 'components/BulkEditor/index';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
|
||||
const headers = collection.draft?.root
|
||||
? get(collection, 'draft.root.request.headers', [])
|
||||
: get(collection, 'root.request.headers', []);
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
setIsBulkEditMode(!isBulkEditMode);
|
||||
};
|
||||
|
||||
const handleBulkHeadersChange = (newHeaders) => {
|
||||
dispatch(setCollectionHeaders({ collectionUid: collection.uid, headers: newHeaders }));
|
||||
};
|
||||
|
||||
const addHeader = () => {
|
||||
dispatch(
|
||||
addCollectionHeader({
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
const handleHeadersChange = useCallback((updatedHeaders) => {
|
||||
dispatch(setCollectionHeaders({ collectionUid: collection.uid, headers: updatedHeaders }));
|
||||
}, [dispatch, collection.uid]);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
const handleHeaderValueChange = (e, _header, type) => {
|
||||
const header = cloneDeep(_header);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
// Strip newlines from header keys
|
||||
header.name = e.target.value.replace(/[\r\n]/g, '');
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
header.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
header.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateCollectionHeader({
|
||||
header: header,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveHeader = (header) => {
|
||||
dispatch(
|
||||
deleteCollectionHeader({
|
||||
headerUid: header.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
const columns = [
|
||||
{
|
||||
key: 'name',
|
||||
name: 'Name',
|
||||
isKeyField: true,
|
||||
placeholder: 'Name',
|
||||
width: '30%',
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(newValue) => onChange(newValue.replace(/[\r\n]/g, ''))}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
collection={collection}
|
||||
placeholder={isLastEmptyRow ? 'Name' : ''}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
name: 'Value',
|
||||
placeholder: 'Value',
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={onChange}
|
||||
collection={collection}
|
||||
autocomplete={MimeTypes}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const defaultRow = {
|
||||
name: '',
|
||||
value: '',
|
||||
description: ''
|
||||
};
|
||||
|
||||
if (isBulkEditMode) {
|
||||
@@ -83,7 +83,7 @@ const Headers = ({ collection }) => {
|
||||
</div>
|
||||
<BulkEditor
|
||||
params={headers}
|
||||
onChange={handleBulkHeadersChange}
|
||||
onChange={handleHeadersChange}
|
||||
onToggle={toggleBulkEditMode}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
@@ -96,92 +96,24 @@ const Headers = ({ collection }) => {
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Add request headers that will be sent with every request in this collection.
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{headers && headers.length
|
||||
? headers.map((header) => {
|
||||
return (
|
||||
<tr key={header.uid}>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={header.name}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'name'
|
||||
)}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={header.value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'value'
|
||||
)}
|
||||
collection={collection}
|
||||
autocomplete={MimeTypes}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={header.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
|
||||
/>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveHeader(header)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex justify-between mt-2">
|
||||
<button className="btn-add-header text-link pr-2 py-3 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<EditableTable
|
||||
columns={columns}
|
||||
rows={headers}
|
||||
onChange={handleHeadersChange}
|
||||
defaultRow={defaultRow}
|
||||
/>
|
||||
<div className="flex justify-end mt-2">
|
||||
<button className="text-link select-none" onClick={toggleBulkEditMode}>
|
||||
Bulk Edit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Headers;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.icon-box {
|
||||
&.location {
|
||||
background-color: ${(props) => rgba(props.theme.textLink, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.textLink, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
&.environments {
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.green, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.colors.text.green, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
}
|
||||
|
||||
&.requests {
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.purple, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.colors.text.purple, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.colors.text.purple};
|
||||
}
|
||||
}
|
||||
|
||||
&.share {
|
||||
background-color: ${(props) => rgba(props.theme.textLink, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.textLink, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
&.generate-docs {
|
||||
background-color: ${(props) => rgba(props.theme.accents.primary, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.accents.primary, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.accents.primary};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,33 +1,44 @@
|
||||
import React from 'react';
|
||||
import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
import { IconBox, IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
|
||||
import { IconFolder, IconWorld, IconApi, IconShare, IconBook } from '@tabler/icons';
|
||||
import { areItemsLoading, getItemsLoadStats } from 'utils/collections/index';
|
||||
import { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import ShareCollection from 'components/ShareCollection/index';
|
||||
import GenerateDocumentation from 'components/Sidebar/Collections/Collection/GenerateDocumentation';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Info = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
|
||||
|
||||
const isCollectionLoading = areItemsLoading(collection);
|
||||
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
|
||||
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
|
||||
const [showGenerateDocumentationModal, setShowGenerateDocumentationModal] = useState(false);
|
||||
|
||||
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
|
||||
|
||||
const collectionEnvironmentCount = collection.environments?.length || 0;
|
||||
const globalEnvironmentCount = globalEnvironments?.length || 0;
|
||||
|
||||
const handleToggleShowShareCollectionModal = (value) => (e) => {
|
||||
toggleShowShareCollectionModal(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col h-fit">
|
||||
<StyledWrapper className="w-full flex flex-col h-fit">
|
||||
<div className="rounded-lg py-6">
|
||||
<div className="grid gap-5">
|
||||
{/* Location Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<IconFolder className="w-5 h-5 text-blue-500" stroke={1.5} />
|
||||
<div className="icon-box location flex-shrink-0 p-3 rounded-lg">
|
||||
<IconFolder className="w-5 h-5" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-medium">Location</div>
|
||||
<div className="mt-1 text-muted break-all text-xs">
|
||||
<div className="mt-1 text-muted break-all">
|
||||
{collection.pathname}
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,25 +46,54 @@ const Info = ({ collection }) => {
|
||||
|
||||
{/* Environments Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<IconWorld className="w-5 h-5 text-green-500" stroke={1.5} />
|
||||
<div className="icon-box environments flex-shrink-0 p-3 rounded-lg">
|
||||
<IconWorld className="w-5 h-5" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-medium">Environments</div>
|
||||
<div className="mt-1 text-muted text-xs">
|
||||
{collection.environments?.length || 0} environment{collection.environments?.length !== 1 ? 's' : ''} configured
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{collectionEnvironmentCount} collection environment{collectionEnvironmentCount !== 1 ? 's' : ''}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-global-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'global-environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{globalEnvironmentCount} global environment{globalEnvironmentCount !== 1 ? 's' : ''}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Requests Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
||||
<IconApi className="w-5 h-5 text-purple-500" stroke={1.5} />
|
||||
<div className="icon-box requests flex-shrink-0 p-3 rounded-lg">
|
||||
<IconApi className="w-5 h-5" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-medium">Requests</div>
|
||||
<div className="mt-1 text-muted text-xs">
|
||||
<div className="mt-1 text-muted">
|
||||
{
|
||||
isCollectionLoading ? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
|
||||
}
|
||||
@@ -62,20 +102,33 @@ const Info = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-start group cursor-pointer" onClick={handleToggleShowShareCollectionModal(true)}>
|
||||
<div className="flex-shrink-0 p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg">
|
||||
<IconShare className="w-5 h-5 text-indigo-500" stroke={1.5} />
|
||||
<div className="icon-box share flex-shrink-0 p-3 rounded-lg">
|
||||
<IconShare className="w-5 h-5" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4 h-full flex flex-col justify-start">
|
||||
<div className="font-medium h-fit my-auto">Share</div>
|
||||
<div className="group-hover:underline text-link text-xs">
|
||||
<div className="group-hover:underline text-link">
|
||||
Share Collection
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showShareCollectionModal && <ShareCollection collectionUid={collection.uid} onClose={handleToggleShowShareCollectionModal(false)} />}
|
||||
|
||||
<div className="flex items-start group cursor-pointer" onClick={() => setShowGenerateDocumentationModal(true)}>
|
||||
<div className="icon-box generate-docs flex-shrink-0 p-3 rounded-lg">
|
||||
<IconBook className="w-5 h-5" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4 h-full flex flex-col justify-start">
|
||||
<div className="font-medium h-fit my-auto">Documentation</div>
|
||||
<div className="group-hover:underline text-link">
|
||||
Generate Docs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showGenerateDocumentationModal && <GenerateDocumentation collectionUid={collection.uid} onClose={() => setShowGenerateDocumentationModal(false)} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
&.card {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.card.bg};
|
||||
|
||||
.title {
|
||||
border-top: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-left: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-right: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-top: 1px solid ${(props) => props.theme.table.border};
|
||||
border-left: 1px solid ${(props) => props.theme.table.border};
|
||||
border-right: 1px solid ${(props) => props.theme.table.border};
|
||||
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
|
||||
background-color: ${(props) => props.theme.status.warning.background};
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
color: ${(props) => props.theme.status.warning.text};
|
||||
}
|
||||
|
||||
.table {
|
||||
thead {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.bg};
|
||||
color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.color};
|
||||
color: ${(props) => props.theme.table.thead.color} !important;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections/index';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
const RequestsNotLoaded = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -21,7 +20,6 @@ const RequestsNotLoaded = ({ collection }) => {
|
||||
const handleRequestClick = (item) => (e) => {
|
||||
e.preventDefault();
|
||||
if (isItemARequest(item)) {
|
||||
dispatch(hideHomePage());
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
focusTab({
|
||||
@@ -43,8 +41,8 @@ const RequestsNotLoaded = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full card my-2">
|
||||
<div className="flex items-center gap-2 px-3 py-2 title bg-yellow-50 dark:bg-yellow-900/20">
|
||||
<IconAlertTriangle size={16} className="text-yellow-500" />
|
||||
<div className="flex items-center gap-2 px-3 py-2 title">
|
||||
<IconAlertTriangle size={16} className="warning-icon" />
|
||||
<span className="font-medium">Following requests were not loaded</span>
|
||||
</div>
|
||||
<table className="w-full border-collapse">
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
.settings-label {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.textbox {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.15rem 0.45rem;
|
||||
box-shadow: none;
|
||||
border-radius: 0px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { updateCollectionPresets } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const PresetsSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const initialPresets = { requestType: 'http', requestUrl: '' };
|
||||
|
||||
// Get presets from draft.brunoConfig if it exists, otherwise from brunoConfig
|
||||
const currentPresets = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.presets', initialPresets)
|
||||
: get(collection, 'brunoConfig.presets', initialPresets);
|
||||
|
||||
// Helper to update presets config
|
||||
const updatePresets = (updates) => {
|
||||
const updatedPresets = { ...currentPresets, ...updates };
|
||||
dispatch(updateCollectionPresets({
|
||||
collectionUid: collection.uid,
|
||||
presets: updatedPresets
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleRequestTypeChange = (e) => {
|
||||
updatePresets({ requestType: e.target.value });
|
||||
};
|
||||
|
||||
const handleRequestUrlChange = (e) => {
|
||||
updatePresets({ requestUrl: e.target.value });
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
These presets will be used as the default values for new requests in this collection.
|
||||
</div>
|
||||
<div className="bruno-form">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="http">
|
||||
Request Type
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="http"
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="http"
|
||||
checked={(currentPresets.requestType || 'http') === 'http'}
|
||||
/>
|
||||
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
|
||||
HTTP
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="graphql"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="graphql"
|
||||
checked={(currentPresets.requestType || 'http') === 'graphql'}
|
||||
/>
|
||||
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
|
||||
GraphQL
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="grpc"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="grpc"
|
||||
checked={(currentPresets.requestType || 'http') === 'grpc'}
|
||||
/>
|
||||
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
|
||||
gRPC
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="ws"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="ws"
|
||||
checked={(currentPresets.requestType || 'http') === 'ws'}
|
||||
/>
|
||||
<label htmlFor="ws" className="ml-1 cursor-pointer select-none">
|
||||
WebSocket
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="request-url">
|
||||
Base URL
|
||||
</label>
|
||||
<div className="flex items-center w-full">
|
||||
<div className="flex items-center flex-grow input-container h-full">
|
||||
<input
|
||||
id="request-url"
|
||||
type="text"
|
||||
name="requestUrl"
|
||||
placeholder="Request URL"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleRequestUrlChange}
|
||||
value={currentPresets.requestUrl || ''}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PresetsSettings;
|
||||
@@ -8,6 +8,151 @@ const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
/* Section labels */
|
||||
label {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
/* Tooltip icon */
|
||||
.tooltip-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Error messages */
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
th {
|
||||
text-align: left;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
&.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* File/Directory icons */
|
||||
.file-icon,
|
||||
.folder-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
/* File/Directory names */
|
||||
.file-name,
|
||||
.directory-name {
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
/* Path text */
|
||||
.path-text {
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
.empty-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
/* Invalid file indicator */
|
||||
.invalid-indicator {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.action-button {
|
||||
padding: 0.25rem;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
transition: all 0.2s;
|
||||
|
||||
&.replace-button {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background-color: ${(props) => props.theme.colors.bg.danger}20;
|
||||
}
|
||||
}
|
||||
|
||||
&.remove-button {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
accent-color: ${(props) => props.theme.colors.accent};
|
||||
border-color: ${(props) => props.theme.table.border};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
}
|
||||
|
||||
/* Add button */
|
||||
.btn-add-param {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getBasename } from 'utils/common/path';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import useProtoFileManagement from '../../../hooks/useProtoFileManagement';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ProtobufSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -112,12 +113,12 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<div className="mb-6" data-testid="protobuf-proto-files-section">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center">
|
||||
<label className="font-medium flex items-center" htmlFor="protoFiles">
|
||||
<label className="flex items-center" htmlFor="protoFiles">
|
||||
Proto Files (
|
||||
{protoFiles.length}
|
||||
)
|
||||
<span id="proto-files-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
<IconAlertCircle size={16} className="tooltip-icon" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="proto-files-tooltip"
|
||||
@@ -130,7 +131,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
|
||||
<div>
|
||||
{protoFiles.some((file) => !file.exists) && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400 mb-2 flex items-center p-2 rounded" data-testid="protobuf-invalid-files-message">
|
||||
<div className="error-message text-xs mb-2 flex items-center p-2" data-testid="protobuf-invalid-files-message">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some proto files cannot be found. Use the replace option to update their locations.
|
||||
</div>
|
||||
@@ -139,13 +140,13 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<table className="w-full border-collapse" data-testid="protobuf-proto-files-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<th>
|
||||
File
|
||||
</th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<th>
|
||||
Path
|
||||
</th>
|
||||
<th className="text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<th className="text-right">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
@@ -153,10 +154,10 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<tbody>
|
||||
{protoFiles.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="3" className="border border-gray-200 dark:border-gray-700 px-3 py-8 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<IconFile size={24} className="text-gray-400 mb-2" />
|
||||
<span className="text-gray-500 dark:text-gray-400">No proto files added</span>
|
||||
<td colSpan="3" className="text-center">
|
||||
<div className="empty-state flex flex-col items-center">
|
||||
<IconFile size={24} className="empty-icon mb-2" />
|
||||
<span className="empty-text">No proto files added</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -166,27 +167,27 @@ const ProtobufSettings = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<IconFile size={16} className="text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100" data-testid="protobuf-proto-file-name">
|
||||
<IconFile size={16} className="file-icon mr-2" />
|
||||
<span className="file-name" data-testid="protobuf-proto-file-name">
|
||||
{getBasename(collection.pathname, file.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
|
||||
{!isValid && <IconAlertCircle size={12} className="invalid-indicator ml-2" />}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 font-mono">
|
||||
<td>
|
||||
<div className="path-text">
|
||||
{file.path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2 text-right">
|
||||
<td className="text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{!isValid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReplaceProtoFile(index)}
|
||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 p-1 rounded"
|
||||
className="action-button replace-button"
|
||||
title="Replace file"
|
||||
>
|
||||
<IconFileImport size={14} />
|
||||
@@ -195,7 +196,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveProtoFile(index)}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 p-1 rounded"
|
||||
className="action-button remove-button"
|
||||
title="Remove file"
|
||||
data-testid="protobuf-remove-file-button"
|
||||
>
|
||||
@@ -219,12 +220,12 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<div className="mb-6" data-testid="protobuf-import-paths-section">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center">
|
||||
<label className="font-medium flex items-center" htmlFor="importPaths">
|
||||
<label className="flex items-center" htmlFor="importPaths">
|
||||
Import Paths (
|
||||
{importPaths.length}
|
||||
)
|
||||
<span id="import-paths-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
<IconAlertCircle size={16} className="tooltip-icon" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="import-paths-tooltip"
|
||||
@@ -237,7 +238,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
|
||||
<div>
|
||||
{importPaths.some((path) => !path.exists) && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400 mb-2 flex items-center p-2 rounded" data-testid="protobuf-invalid-import-paths-message">
|
||||
<div className="error-message text-xs mb-2 flex items-center p-2" data-testid="protobuf-invalid-import-paths-message">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some import paths cannot be found at their specified locations.
|
||||
</div>
|
||||
@@ -246,15 +247,15 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<table className="w-full border-collapse" data-testid="protobuf-import-paths-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<th>
|
||||
</th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<th>
|
||||
Directory
|
||||
</th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<th>
|
||||
Path
|
||||
</th>
|
||||
<th className="text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<th className="text-right">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
@@ -262,10 +263,10 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<tbody>
|
||||
{importPaths.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="4" className="border border-gray-200 dark:border-gray-700 px-3 py-8 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<IconFolder size={24} className="text-gray-400 mb-2" />
|
||||
<span className="text-gray-500 dark:text-gray-400">No import paths added</span>
|
||||
<td colSpan="4" className="text-center">
|
||||
<div className="empty-state flex flex-col items-center">
|
||||
<IconFolder size={24} className="empty-icon mb-2" />
|
||||
<span className="empty-text">No import paths added</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -275,37 +276,37 @@ const ProtobufSettings = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={importPath.enabled}
|
||||
onChange={() => handleToggleImportPath(index)}
|
||||
className="h-4 w-4 text-gray-600 focus:ring-gray-500 border-gray-300 dark:border-gray-600 rounded"
|
||||
className="h-4 w-4"
|
||||
title={importPath.enabled ? 'Disable this import path' : 'Enable this import path'}
|
||||
data-testid="protobuf-import-path-checkbox"
|
||||
/>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<IconFolder size={16} className="text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
<IconFolder size={16} className="folder-icon mr-2" />
|
||||
<span className="directory-name">
|
||||
{getBasename(collection.pathname, importPath.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
|
||||
{!isValid && <IconAlertCircle size={12} className="invalid-indicator ml-2" />}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 font-mono">
|
||||
<td>
|
||||
<div className="path-text">
|
||||
{importPath.path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2 text-right">
|
||||
<td className="text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{!isValid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReplaceImportPath(index)}
|
||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 p-1 rounded"
|
||||
className="action-button replace-button"
|
||||
title="Replace directory"
|
||||
>
|
||||
<IconFileImport size={14} />
|
||||
@@ -314,7 +315,7 @@ const ProtobufSettings = ({ collection }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveImportPath(index)}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 p-1 rounded"
|
||||
className="action-button remove-button"
|
||||
title="Remove import path"
|
||||
data-testid="protobuf-remove-import-path-button"
|
||||
>
|
||||
@@ -335,9 +336,9 @@ const ProtobufSettings = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -14,11 +14,11 @@ const StyledWrapper = styled.div`
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
border: solid 1px ${(props) => props.theme.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,23 @@ import { updateCollectionProxy } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
import toast from 'react-hot-toast';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ProxySettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const initialProxyConfig = { enabled: 'global', protocol: 'http', hostname: '', port: '', auth: { enabled: false, username: '', password: '' }, bypassProxy: '' };
|
||||
const initialProxyConfig = {
|
||||
inherit: true,
|
||||
config: {
|
||||
protocol: 'http',
|
||||
hostname: '',
|
||||
port: '',
|
||||
auth: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
bypassProxy: ''
|
||||
}
|
||||
};
|
||||
|
||||
// Get proxy from draft.brunoConfig if it exists, otherwise from brunoConfig
|
||||
const currentProxyConfig = collection.draft?.brunoConfig
|
||||
@@ -81,34 +94,57 @@ const ProxySettings = ({ collection }) => {
|
||||
|
||||
const handleEnabledChange = (e) => {
|
||||
const value = e.target.value;
|
||||
// Convert string to boolean or keep as 'global'
|
||||
const enabled = value === 'true' ? true : value === 'false' ? false : 'global';
|
||||
updateProxy({ enabled });
|
||||
// Map UI values to new format
|
||||
if (value === 'inherit') {
|
||||
updateProxy({ disabled: false, inherit: true });
|
||||
} else if (value === 'true') {
|
||||
updateProxy({ disabled: false, inherit: false });
|
||||
} else {
|
||||
updateProxy({ disabled: true, inherit: false });
|
||||
}
|
||||
};
|
||||
|
||||
const handleProtocolChange = (e) => {
|
||||
updateProxy({ protocol: e.target.value });
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
protocol: e.target.value
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleHostnameChange = (e) => {
|
||||
const hostname = e.target.value;
|
||||
if (validateHostnameOnChange(hostname)) {
|
||||
updateProxy({ hostname });
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
hostname
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePortChange = (e) => {
|
||||
const port = e.target.value ? Number(e.target.value) : '';
|
||||
if (validatePortOnChange(port)) {
|
||||
updateProxy({ port });
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
port
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthEnabledChange = (e) => {
|
||||
updateProxy({
|
||||
auth: {
|
||||
...currentProxyConfig.auth,
|
||||
enabled: e.target.checked
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
auth: {
|
||||
...currentProxyConfig.config.auth,
|
||||
disabled: !e.target.checked
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -117,9 +153,12 @@ const ProxySettings = ({ collection }) => {
|
||||
const username = e.target.value;
|
||||
if (validateAuthUsernameOnChange(username)) {
|
||||
updateProxy({
|
||||
auth: {
|
||||
...currentProxyConfig.auth,
|
||||
username
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
auth: {
|
||||
...currentProxyConfig.config.auth,
|
||||
username
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -129,9 +168,12 @@ const ProxySettings = ({ collection }) => {
|
||||
const password = e.target.value;
|
||||
if (validateAuthPasswordOnChange(password)) {
|
||||
updateProxy({
|
||||
auth: {
|
||||
...currentProxyConfig.auth,
|
||||
password
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
auth: {
|
||||
...currentProxyConfig.config.auth,
|
||||
password
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -140,11 +182,19 @@ const ProxySettings = ({ collection }) => {
|
||||
const handleBypassProxyChange = (e) => {
|
||||
const bypassProxy = e.target.value;
|
||||
if (validateBypassProxyOnChange(bypassProxy)) {
|
||||
updateProxy({ bypassProxy });
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
bypassProxy
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const enabledValue = currentProxyConfig.enabled === true ? 'true' : currentProxyConfig.enabled === false ? 'false' : 'global';
|
||||
// Map new format to UI values
|
||||
const disabled = currentProxyConfig.disabled || false;
|
||||
const inherit = currentProxyConfig.inherit !== undefined ? currentProxyConfig.inherit : true;
|
||||
const enabledValue = disabled ? 'false' : (inherit ? 'inherit' : 'true');
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
@@ -156,9 +206,9 @@ const ProxySettings = ({ collection }) => {
|
||||
<InfoTip infotipId="request-var">
|
||||
<div>
|
||||
<ul>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>global</span> - use global proxy config</li>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>enabled</span> - use collection proxy config</li>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>disable</span> - disable proxy</li>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>inherit</span> - inherit from global preferences</li>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>enabled</span> - use collection-specific proxy config</li>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>disabled</span> - disable proxy for this collection</li>
|
||||
</ul>
|
||||
</div>
|
||||
</InfoTip>
|
||||
@@ -168,12 +218,12 @@ const ProxySettings = ({ collection }) => {
|
||||
<input
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value="global"
|
||||
checked={enabledValue === 'global'}
|
||||
value="inherit"
|
||||
checked={enabledValue === 'inherit'}
|
||||
onChange={handleEnabledChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
global
|
||||
inherit
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
@@ -199,168 +249,172 @@ const ProxySettings = ({ collection }) => {
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="protocol">
|
||||
Protocol
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
{enabledValue === 'true' && (
|
||||
<>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="protocol">
|
||||
Protocol
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="http"
|
||||
checked={(currentProxyConfig.config?.protocol || 'http') === 'http'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTP
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="https"
|
||||
checked={(currentProxyConfig.config?.protocol || 'http') === 'https'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTPS
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks4"
|
||||
checked={(currentProxyConfig.config?.protocol || 'http') === 'socks4'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS4
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={(currentProxyConfig.config?.protocol || 'http') === 'socks5'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS5
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="hostname">
|
||||
Hostname
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="http"
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'http'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTP
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="https"
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'https'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTPS
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks4"
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'socks4'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS4
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'socks5'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS5
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="hostname">
|
||||
Hostname
|
||||
</label>
|
||||
<input
|
||||
id="hostname"
|
||||
type="text"
|
||||
name="hostname"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleHostnameChange}
|
||||
value={currentProxyConfig.hostname || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
id="port"
|
||||
type="number"
|
||||
name="port"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handlePortChange}
|
||||
value={currentProxyConfig.port || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.enabled">
|
||||
Auth
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auth.enabled"
|
||||
checked={currentProxyConfig.auth?.enabled || false}
|
||||
onChange={handleAuthEnabledChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.username">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="auth.username"
|
||||
type="text"
|
||||
name="auth.username"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={currentProxyConfig.auth?.username || ''}
|
||||
onChange={handleAuthUsernameChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
Password
|
||||
</label>
|
||||
<div className="textbox flex flex-row items-center w-[13.2rem] h-[1.70rem] relative">
|
||||
<input
|
||||
id="auth.password"
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
name="auth.password"
|
||||
className="outline-none bg-transparent w-[10.5rem]"
|
||||
id="hostname"
|
||||
type="text"
|
||||
name="hostname"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={currentProxyConfig.auth?.password || ''}
|
||||
onChange={handleAuthPasswordChange}
|
||||
onChange={handleHostnameChange}
|
||||
value={currentProxyConfig.config?.hostname || ''}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm absolute right-0"
|
||||
onClick={() => setPasswordVisible(!passwordVisible)}
|
||||
>
|
||||
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="bypassProxy">
|
||||
Proxy Bypass
|
||||
</label>
|
||||
<input
|
||||
id="bypassProxy"
|
||||
type="text"
|
||||
name="bypassProxy"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleBypassProxyChange}
|
||||
value={currentProxyConfig.bypassProxy || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
id="port"
|
||||
type="number"
|
||||
name="port"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handlePortChange}
|
||||
value={currentProxyConfig.config?.port || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.disabled">
|
||||
Auth
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auth.disabled"
|
||||
checked={!currentProxyConfig.config?.auth?.disabled}
|
||||
onChange={handleAuthEnabledChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.username">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="auth.username"
|
||||
type="text"
|
||||
name="auth.username"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={currentProxyConfig.config?.auth?.username || ''}
|
||||
onChange={handleAuthUsernameChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
Password
|
||||
</label>
|
||||
<div className="textbox flex flex-row items-center w-[13.2rem] h-[1.70rem] relative">
|
||||
<input
|
||||
id="auth.password"
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
name="auth.password"
|
||||
className="outline-none bg-transparent w-[10.5rem]"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={currentProxyConfig.config?.auth?.password || ''}
|
||||
onChange={handleAuthPasswordChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm absolute right-0"
|
||||
onClick={() => setPasswordVisible(!passwordVisible)}
|
||||
>
|
||||
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="bypassProxy">
|
||||
Proxy Bypass
|
||||
</label>
|
||||
<input
|
||||
id="bypassProxy"
|
||||
type="text"
|
||||
name="bypassProxy"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleBypassProxyChange}
|
||||
value={currentProxyConfig.config?.bypassProxy || ''}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -8,7 +8,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Script = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -55,7 +56,7 @@ const Script = ({ collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full pt-4">
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Write pre and post-request scripts that will run before and after any request in this collection is sent.
|
||||
</div>
|
||||
@@ -98,9 +99,9 @@ const Script = ({ collection }) => {
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -6,8 +6,8 @@ const StyledWrapper = styled.div`
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
@@ -19,7 +19,12 @@ const StyledWrapper = styled.div`
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
@@ -39,6 +44,11 @@ const StyledWrapper = styled.div`
|
||||
.muted {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
cursor: pointer;
|
||||
accent-color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { updateCollectionTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Tests = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -41,9 +42,9 @@ const Tests = ({ collection }) => {
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,160 +1,81 @@
|
||||
import React from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import MultiLineEditor from 'components/MultiLineEditor';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import EditableTable from 'components/EditableTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import {
|
||||
addCollectionVar,
|
||||
deleteCollectionVar,
|
||||
updateCollectionVar
|
||||
} from 'providers/ReduxStore/slices/collections/index';
|
||||
import { setCollectionVars } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const VarsTable = ({ collection, vars, varType }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const addVar = () => {
|
||||
dispatch(
|
||||
addCollectionVar({
|
||||
collectionUid: collection.uid,
|
||||
type: varType
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
const handleVarChange = (e, v, type) => {
|
||||
const _var = cloneDeep(v);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
const value = e.target.value;
|
||||
|
||||
if (variableNameRegex.test(value) === false) {
|
||||
toast.error(
|
||||
'Variable contains invalid characters! Variables must only contain alpha-numeric characters, "-", "_", "."'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const handleVarsChange = useCallback((updatedVars) => {
|
||||
dispatch(setCollectionVars({ collectionUid: collection.uid, vars: updatedVars, type: varType }));
|
||||
}, [dispatch, collection.uid, varType]);
|
||||
|
||||
_var.name = value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
_var.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
_var.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
const getRowError = useCallback((row, index, key) => {
|
||||
if (key !== 'name') return null;
|
||||
if (!row.name || row.name.trim() === '') return null;
|
||||
if (!variableNameRegex.test(row.name)) {
|
||||
return 'Variable contains invalid characters. Must only contain alphanumeric characters, "-", "_", "."';
|
||||
}
|
||||
dispatch(
|
||||
updateCollectionVar({
|
||||
type: varType,
|
||||
var: _var,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const handleRemoveVar = (_var) => {
|
||||
dispatch(
|
||||
deleteCollectionVar({
|
||||
type: varType,
|
||||
varUid: _var.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
const columns = [
|
||||
{
|
||||
key: 'name',
|
||||
name: 'Name',
|
||||
isKeyField: true,
|
||||
placeholder: 'Name',
|
||||
width: '40%'
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
name: varType === 'request' ? 'Value' : (
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip content="You can write any valid JS Template Literal here" infotipId={`collection-${varType}-var`} />
|
||||
</div>
|
||||
),
|
||||
placeholder: varType === 'request' ? 'Value' : 'Expr',
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<MultiLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={onChange}
|
||||
collection={collection}
|
||||
placeholder={isLastEmptyRow ? (varType === 'request' ? 'Value' : 'Expr') : ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const defaultRow = {
|
||||
name: '',
|
||||
value: '',
|
||||
...(varType === 'response' ? { local: false } : {})
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
{varType === 'request' ? (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Value</span>
|
||||
</div>
|
||||
</td>
|
||||
) : (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip content="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vars && vars.length
|
||||
? vars.map((_var) => {
|
||||
return (
|
||||
<tr key={_var.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={_var.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<MultiLineEditor
|
||||
value={_var.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleVarChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
_var,
|
||||
'value'
|
||||
)}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={_var.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
||||
/>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={addVar}>
|
||||
+ Add
|
||||
</button>
|
||||
<EditableTable
|
||||
columns={columns}
|
||||
rows={vars}
|
||||
onChange={handleVarsChange}
|
||||
defaultRow={defaultRow}
|
||||
getRowError={getRowError}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default VarsTable;
|
||||
|
||||
@@ -4,22 +4,28 @@ import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import DeprecationWarning from 'components/DeprecationWarning';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Vars = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="flex-1">
|
||||
<div className="mb-3 title text-xs">Pre Request</div>
|
||||
<VarsTable collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mt-3 mb-3 title text-xs">Post Response</div>
|
||||
<VarsTable collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import Headers from './Headers';
|
||||
import Auth from './Auth';
|
||||
import Script from './Script';
|
||||
import Test from './Tests';
|
||||
import Presets from './Presets';
|
||||
import Protobuf from './Protobuf';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
@@ -32,17 +33,34 @@ const CollectionSettings = ({ collection }) => {
|
||||
const hasTests = root?.request?.tests;
|
||||
const hasDocs = root?.docs;
|
||||
|
||||
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
|
||||
const headers = collection.draft?.root
|
||||
? get(collection, 'draft.root.request.headers', [])
|
||||
: get(collection, 'root.request.headers', []);
|
||||
const activeHeadersCount = headers.filter((header) => header.enabled).length;
|
||||
|
||||
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length;
|
||||
const authMode = (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {})).mode || 'none';
|
||||
const requestVars = collection.draft?.root
|
||||
? get(collection, 'draft.root.request.vars.req', [])
|
||||
: get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = collection.draft?.root
|
||||
? get(collection, 'draft.root.request.vars.res', [])
|
||||
: get(collection, 'root.request.vars.res', []);
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
const authMode
|
||||
= (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {}))
|
||||
.mode || 'none';
|
||||
|
||||
const proxyConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.proxy', {}) : get(collection, 'brunoConfig.proxy', {});
|
||||
const proxyConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.proxy', {})
|
||||
: get(collection, 'brunoConfig.proxy', {});
|
||||
const proxyEnabled = proxyConfig.hostname ? true : false;
|
||||
const clientCertConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.clientCertificates.certs', []) : get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const protobufConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.protobuf', {}) : get(collection, 'brunoConfig.protobuf', {});
|
||||
const clientCertConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.clientCertificates.certs', [])
|
||||
: get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const protobufConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.protobuf', {})
|
||||
: get(collection, 'brunoConfig.protobuf', {});
|
||||
const presets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', {}) : get(collection, 'brunoConfig.presets', {});
|
||||
const hasPresets = presets && presets.requestUrl !== '';
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
@@ -64,15 +82,14 @@ const CollectionSettings = ({ collection }) => {
|
||||
case 'tests': {
|
||||
return <Test collection={collection} />;
|
||||
}
|
||||
case 'presets': {
|
||||
return <Presets collection={collection} />;
|
||||
}
|
||||
case 'proxy': {
|
||||
return <ProxySettings collection={collection} />;
|
||||
}
|
||||
case 'clientCert': {
|
||||
return (
|
||||
<ClientCertSettings
|
||||
collection={collection}
|
||||
/>
|
||||
);
|
||||
return <ClientCertSettings collection={collection} />;
|
||||
}
|
||||
case 'protobuf': {
|
||||
return <Protobuf collection={collection} />;
|
||||
@@ -112,6 +129,10 @@ const CollectionSettings = ({ collection }) => {
|
||||
Tests
|
||||
{hasTests && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('presets')} role="tab" onClick={() => setTab('presets')}>
|
||||
Presets
|
||||
{hasPresets && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
{Object.keys(proxyConfig).length > 0 && proxyEnabled && <StatusDot />}
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
const StyledWrapper = styled.div`
|
||||
/* Info icon */
|
||||
.info-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
/* Required field asterisk */
|
||||
.required-asterisk {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
/* Error messages */
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
accent-color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -11,6 +11,7 @@ import moment from 'moment';
|
||||
import 'moment-timezone';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { isEmpty } from 'lodash';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const removeEmptyValues = (obj) => {
|
||||
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null && value !== undefined));
|
||||
@@ -225,145 +226,147 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<form onSubmit={(e) => e.preventDefault()} className="px-2">
|
||||
{isRawMode ? (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<label className="block">Set-Cookie String</label>
|
||||
<IconInfoCircle id="cookie-raw-info" size={16} strokeWidth={1.5} className="text-gray-400" />
|
||||
<Tooltip
|
||||
anchorId="cookie-raw-info"
|
||||
className="tooltip-mod"
|
||||
html="Key, Path, and Domain are immutable properties and cannot be modified for existing cookies"
|
||||
<StyledWrapper>
|
||||
<form onSubmit={(e) => e.preventDefault()} className="px-2">
|
||||
{isRawMode ? (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<label className="block">Set-Cookie String</label>
|
||||
<IconInfoCircle id="cookie-raw-info" size={16} strokeWidth={1.5} className="info-icon" />
|
||||
<Tooltip
|
||||
anchorId="cookie-raw-info"
|
||||
className="tooltip-mod"
|
||||
html="Key, Path, and Domain are immutable properties and cannot be modified for existing cookies"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
value={cookieString}
|
||||
onChange={(e) => setCookieString(e.target.value)}
|
||||
className="block textbox w-full h-24"
|
||||
placeholder="key=value; key2=value2"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
value={cookieString}
|
||||
onChange={(e) => setCookieString(e.target.value)}
|
||||
className="block textbox w-full h-24"
|
||||
placeholder="key=value; key2=value2"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block mb-1">
|
||||
Domain<span className="text-red-600">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="domain"
|
||||
// Auto-focus if its add-new i.e. when domain prop is empty
|
||||
autoFocus={!domain && !formik.values.domain}
|
||||
value={formik.values.domain}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.domain && formik.errors.domain && (
|
||||
<div className="text-red-500 mt-1">{formik.errors.domain}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1">Path</label>
|
||||
<input
|
||||
type="text"
|
||||
name="path"
|
||||
value={formik.values.path}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.path && formik.errors.path && (
|
||||
<div className="text-red-500 mt-1">{formik.errors.path}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1">
|
||||
Key<span className="text-red-600">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="key"
|
||||
// Auto focus when add-for-domain i.e. if domain is already prefilled
|
||||
autoFocus={!!domain && !formik.values.key}
|
||||
value={formik.values.key}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.key && formik.errors.key && (
|
||||
<div className="text-red-500 mt-1">{formik.errors.key}</div>
|
||||
)}
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block mb-1">
|
||||
Domain<span className="required-asterisk">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="domain"
|
||||
// Auto-focus if its add-new i.e. when domain prop is empty
|
||||
autoFocus={!domain && !formik.values.domain}
|
||||
value={formik.values.domain}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.domain && formik.errors.domain && (
|
||||
<div className="error-message mt-1">{formik.errors.domain}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1">Path</label>
|
||||
<input
|
||||
type="text"
|
||||
name="path"
|
||||
value={formik.values.path}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.path && formik.errors.path && (
|
||||
<div className="error-message mt-1">{formik.errors.path}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block mb-1">
|
||||
Key<span className="required-asterisk">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="key"
|
||||
// Auto focus when add-for-domain i.e. if domain is already prefilled
|
||||
autoFocus={!!domain && !formik.values.key}
|
||||
value={formik.values.key}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.key && formik.errors.key && (
|
||||
<div className="error-message mt-1">{formik.errors.key}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1">
|
||||
Value<span className="required-asterisk">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="value"
|
||||
// Auto-focus when its in edit mode i.e. cookie prop is present
|
||||
autoFocus={!!cookie}
|
||||
value={formik.values.value}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full"
|
||||
/>
|
||||
{formik.touched.value && formik.errors.value && (
|
||||
<div className="error-message mt-1">{formik.errors.value}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-1">
|
||||
Value<span className="text-red-600">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="value"
|
||||
// Auto-focus when its in edit mode i.e. cookie prop is present
|
||||
autoFocus={!!cookie}
|
||||
value={formik.values.value}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full"
|
||||
/>
|
||||
{formik.touched.value && formik.errors.value && (
|
||||
<div className="text-red-500 mt-1">{formik.errors.value}</div>
|
||||
)}
|
||||
{/* Date Picker */}
|
||||
<div className="w-full flex items-end">
|
||||
<div>
|
||||
<label className="block mb-1">Expiration ({moment.tz.guess()})</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="expires"
|
||||
value={formik.values.expires}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
}}
|
||||
className="block textbox non-passphrase-input w-full"
|
||||
min={moment().format(moment.HTML5_FMT.DATETIME_LOCAL)}
|
||||
/>
|
||||
{formik.touched.expires && formik.errors.expires && (
|
||||
<div className="error-message mt-1">{formik.errors.expires}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Checkboxes */}
|
||||
<div className="flex space-x-4 ml-auto">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="secure"
|
||||
checked={formik.values.secure}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span>Secure</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="httpOnly"
|
||||
checked={formik.values.httpOnly}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span>HTTP Only</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date Picker */}
|
||||
<div className="w-full flex items-end">
|
||||
<div>
|
||||
<label className="block mb-1">Expiration ({moment.tz.guess()})</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="expires"
|
||||
value={formik.values.expires}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
}}
|
||||
className="block textbox non-passphrase-input w-full"
|
||||
min={moment().format(moment.HTML5_FMT.DATETIME_LOCAL)}
|
||||
/>
|
||||
{formik.touched.expires && formik.errors.expires && (
|
||||
<div className="text-red-500 mt-1">{formik.errors.expires}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Checkboxes */}
|
||||
<div className="flex space-x-4 ml-auto">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="secure"
|
||||
checked={formik.values.secure}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span>Secure</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="httpOnly"
|
||||
checked={formik.values.httpOnly}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span>HTTP Only</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,11 +28,11 @@ const Wrapper = styled.div`
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
border: solid 1px ${(props) => props.theme.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,92 @@ const Wrapper = styled.div`
|
||||
background-size: 100% 30px, 100% 30px, 100% 10px, 100% 10px;
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
|
||||
/* Warning icon */
|
||||
.warning-icon {
|
||||
color: ${(props) => props.theme.colors.text.warning};
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
/* Domain count text */
|
||||
.domain-count {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.action-button {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
transition: color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
.action-button-danger {
|
||||
color: ${(props) => props.theme.text};
|
||||
transition: color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
/* Table styles */
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
border-bottom: 1px solid ${(props) => props.theme.table.border};
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
|
||||
th {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid ${(props) => props.theme.table.border};
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Edit button */
|
||||
.edit-button {
|
||||
color: ${(props) => props.theme.text};
|
||||
transition: color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete button */
|
||||
.delete-button {
|
||||
color: ${(props) => props.theme.text};
|
||||
transition: color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -9,11 +9,12 @@ import ModifyCookieModal from 'components/Cookies/ModifyCookieModal/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import moment from 'moment';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
|
||||
<Modal onClose={onClose} handleCancel={onClose} title="Clear Domain Cookies" hideFooter={true}>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="warning-icon" />
|
||||
<h1 className="ml-2 text-lg font-medium">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
@@ -22,14 +23,14 @@ const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-close" onClick={onClose}>
|
||||
<Button color="secondary" variant="ghost" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onClear}>
|
||||
<Button color="danger" onClick={onClear}>
|
||||
Clear All
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -38,7 +39,7 @@ const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
|
||||
const DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (
|
||||
<Modal onClose={onClose} handleCancel={onClose} title="Delete Cookie" hideFooter={true}>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="warning-icon" />
|
||||
<h1 className="ml-2 text-lg font-medium">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
@@ -47,14 +48,14 @@ const DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-close" onClick={onClose}>
|
||||
<Button color="secondary" variant="ghost" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onDelete}>
|
||||
<Button color="danger" onClick={onDelete}>
|
||||
Delete
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -137,18 +138,20 @@ const CollectionProperties = ({ onClose }) => {
|
||||
value={searchText || ''}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className="block textbox non-passphrase-input ml-auto font-normal"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
className="submit btn btn-sm btn-secondary flex items-center gap-1 mx-4 font-medium"
|
||||
size="sm"
|
||||
className="mx-4"
|
||||
icon={<IconCirclePlus strokeWidth={1.5} size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie();
|
||||
}}
|
||||
>
|
||||
<IconCirclePlus strokeWidth={1.5} size={16} />
|
||||
<span>Add Cookie</span>
|
||||
</button>
|
||||
</Button>
|
||||
</StyledWrapper>
|
||||
) : null}
|
||||
>
|
||||
@@ -156,27 +159,28 @@ const CollectionProperties = ({ onClose }) => {
|
||||
{!cookies || !cookies.length ? (
|
||||
// No cookies found
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<IconCookieOff size={48} strokeWidth={1.5} className="text-gray-500" />
|
||||
<IconCookieOff size={48} strokeWidth={1.5} className="empty-icon" />
|
||||
<h2 className="text-lg font-medium mt-4">No cookies found</h2>
|
||||
<p className="text-gray-500 mt-2">Add cookies to get started</p>
|
||||
<button
|
||||
<p className="empty-text mt-2">Add cookies to get started</p>
|
||||
<Button
|
||||
type="submit"
|
||||
className="submit btn btn-sm btn-secondary flex items-center gap-1 mt-8"
|
||||
size="sm"
|
||||
className="mt-8"
|
||||
icon={<IconCirclePlus strokeWidth={1.5} size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie();
|
||||
}}
|
||||
>
|
||||
<IconCirclePlus strokeWidth={1.5} size={16} />
|
||||
<span>Add Cookie</span>
|
||||
</button>
|
||||
Add Cookie
|
||||
</Button>
|
||||
</div>
|
||||
) : cookies.length && !filteredCookies.length ? (
|
||||
// No search results
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<IconSearch size={48} />
|
||||
<h2 className="text-lg font-medium mt-4">No search results</h2>
|
||||
<p className="text-gray-500 mt-2">Try a different search term</p>
|
||||
<p className="empty-text mt-2">Try a different search term</p>
|
||||
</div>
|
||||
) : (
|
||||
// Show cookies list
|
||||
@@ -187,14 +191,14 @@ const CollectionProperties = ({ onClose }) => {
|
||||
<Accordion.Header index={i} className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span>{domainWithCookies.domain}</span>
|
||||
<span className="ml-2 text-xs dark:text-gray-300 text-gray-500">
|
||||
<span className="domain-count ml-2 text-xs">
|
||||
({domainWithCookies.cookies.length}{' '}
|
||||
{domainWithCookies.cookies.length === 1 ? 'cookie' : 'cookies'})
|
||||
</span>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center gap-1 text-gray-500 hover:text-gray-950 dark:text-white dark:hover:text-gray-300"
|
||||
className="action-button flex items-center gap-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie(domainWithCookies.domain);
|
||||
@@ -207,7 +211,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
e.stopPropagation();
|
||||
handleClearDomainCookies(domainWithCookies.domain);
|
||||
}}
|
||||
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600 mr-2"
|
||||
className="action-button-danger mr-2"
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
@@ -218,7 +222,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
<div className="flex items-center justify-between">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="text-left border-b border-gray-200 dark:border-neutral-600 text-gray-700 dark:text-gray-300">
|
||||
<tr className="text-left">
|
||||
<th className="py-2 px-4 font-medium w-32">Name</th>
|
||||
<th className="py-2 px-4 font-medium w-52">Value</th>
|
||||
<th className="py-2 px-4 font-medium">Path</th>
|
||||
@@ -230,7 +234,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
</thead>
|
||||
<tbody>
|
||||
{domainWithCookies.cookies.map((cookie) => (
|
||||
<tr key={cookie.key} className="border-b border-gray-200 dark:border-neutral-600 last:border-none">
|
||||
<tr key={cookie.key}>
|
||||
<td className="py-2 px-4 truncate">
|
||||
<span id={`cookie-key-${cookie.key}`}>{cookie.key}</span>
|
||||
<Tooltip
|
||||
@@ -271,8 +275,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
e.stopPropagation();
|
||||
handleEditCookie(domainWithCookies.domain, cookie);
|
||||
}}
|
||||
className="text-gray-700 hover:text-gray-950
|
||||
dark:text-white dark:hover:text-gray-300"
|
||||
className="edit-button"
|
||||
>
|
||||
<IconEdit strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
@@ -281,7 +284,7 @@ const CollectionProperties = ({ onClose }) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteCookie(domainWithCookies.domain, cookie.path, cookie.key);
|
||||
}}
|
||||
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600"
|
||||
className="delete-button"
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
152
packages/bruno-app/src/components/CreateUntitledRequest/index.js
Normal file
152
packages/bruno-app/src/components/CreateUntitledRequest/index.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import { newHttpRequest, newGrpcRequest, newWsRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { generateUniqueRequestName } from 'utils/collections';
|
||||
import { sanitizeName } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconApi, IconBrandGraphql, IconPlugConnected, IconCode, IconPlus } from '@tabler/icons';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
|
||||
const CreateUntitledRequest = ({ collectionUid, itemUid = null, onRequestCreated, placement = 'bottom' }) => {
|
||||
const dispatch = useDispatch();
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const collection = collections?.find((c) => c.uid === collectionUid);
|
||||
|
||||
const handleCreateHttpRequest = useCallback(async () => {
|
||||
const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
dispatch(
|
||||
newHttpRequest({
|
||||
requestName: uniqueName,
|
||||
filename: filename,
|
||||
requestType: 'http-request',
|
||||
requestUrl: '',
|
||||
requestMethod: 'GET',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: itemUid
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('New request created!');
|
||||
onRequestCreated?.();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
}, [dispatch, collection, itemUid, onRequestCreated]);
|
||||
|
||||
const handleCreateGraphQLRequest = useCallback(async () => {
|
||||
const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
dispatch(
|
||||
newHttpRequest({
|
||||
requestName: uniqueName,
|
||||
filename: filename,
|
||||
requestType: 'graphql-request',
|
||||
requestUrl: '',
|
||||
requestMethod: 'POST',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: itemUid,
|
||||
body: {
|
||||
mode: 'graphql',
|
||||
graphql: {
|
||||
query: '',
|
||||
variables: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('New request created!');
|
||||
onRequestCreated?.();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
}, [dispatch, collection, itemUid, onRequestCreated]);
|
||||
|
||||
const handleCreateWebSocketRequest = useCallback(async () => {
|
||||
const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
dispatch(
|
||||
newWsRequest({
|
||||
requestName: uniqueName,
|
||||
filename: filename,
|
||||
requestUrl: '',
|
||||
requestMethod: 'ws',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: itemUid
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('New request created!');
|
||||
onRequestCreated?.();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
}, [dispatch, collection, itemUid, onRequestCreated]);
|
||||
|
||||
const handleCreateGrpcRequest = useCallback(async () => {
|
||||
const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);
|
||||
const filename = sanitizeName(uniqueName);
|
||||
|
||||
dispatch(
|
||||
newGrpcRequest({
|
||||
requestName: uniqueName,
|
||||
filename: filename,
|
||||
requestUrl: '',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: itemUid
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('New request created!');
|
||||
onRequestCreated?.();
|
||||
})
|
||||
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
|
||||
}, [dispatch, collection, itemUid, onRequestCreated]);
|
||||
|
||||
const menuItems = useMemo(() => [
|
||||
{
|
||||
id: 'http',
|
||||
label: 'HTTP',
|
||||
leftSection: <IconApi size={16} strokeWidth={2} />,
|
||||
onClick: handleCreateHttpRequest
|
||||
},
|
||||
{
|
||||
id: 'graphql',
|
||||
label: 'GraphQL',
|
||||
leftSection: <IconBrandGraphql size={16} strokeWidth={2} />,
|
||||
onClick: handleCreateGraphQLRequest
|
||||
},
|
||||
{
|
||||
id: 'websocket',
|
||||
label: 'WebSocket',
|
||||
leftSection: <IconPlugConnected size={16} strokeWidth={2} />,
|
||||
onClick: handleCreateWebSocketRequest
|
||||
},
|
||||
{
|
||||
id: 'grpc',
|
||||
label: 'gRPC',
|
||||
leftSection: <IconCode size={16} strokeWidth={2} />,
|
||||
onClick: handleCreateGrpcRequest
|
||||
}
|
||||
], [handleCreateHttpRequest, handleCreateGraphQLRequest, handleCreateWebSocketRequest, handleCreateGrpcRequest]);
|
||||
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuDropdown
|
||||
items={menuItems}
|
||||
placement={placement}
|
||||
autoFocusFirstOption={true}
|
||||
>
|
||||
<ActionIcon size="sm">
|
||||
<IconPlus size={16} strokeWidth={2} />
|
||||
</ActionIcon>
|
||||
</MenuDropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUntitledRequest;
|
||||
@@ -38,20 +38,6 @@ const StyledWrapper = styled.div`
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: transparent;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.debug-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -32,12 +32,6 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.network-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.network-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
@@ -81,11 +75,10 @@ const StyledWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 150px 1fr 100px 80px 80px;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
padding: 4px 16px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
font-size: 10px;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
@@ -103,8 +96,7 @@ const StyledWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 80px 80px 150px 1fr 100px 80px 80px;
|
||||
gap: 12px;
|
||||
padding: 6px 16px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
padding: 2px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
@@ -115,7 +107,8 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
padding-left: 13px;
|
||||
background: ${(props) => props.theme.console.logHoverBg};
|
||||
border-left: 3px solid ${(props) => props.theme.console.checkboxColor};
|
||||
}
|
||||
}
|
||||
@@ -123,25 +116,19 @@ const StyledWrapper = styled.div`
|
||||
.method-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
justify-content: start;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-weight: 500;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
}
|
||||
|
||||
.request-domain {
|
||||
color: ${(props) => props.theme.console.messageColor};
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -174,120 +161,6 @@ const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.filter-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.filter-dropdown-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
}
|
||||
|
||||
.filter-summary {
|
||||
font-weight: 500;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
max-width: 250px;
|
||||
background: ${(props) => props.theme.console.dropdownBg};
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-dropdown-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: ${(props) => props.theme.console.dropdownHeaderBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
}
|
||||
|
||||
.filter-toggle-all {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
cursor: pointer;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
.filter-dropdown-options {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.filter-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.optionHoverBg};
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin: 0 8px 0 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: ${(props) => props.theme.console.checkboxColor};
|
||||
}
|
||||
}
|
||||
|
||||
.filter-option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.filter-option-label {
|
||||
color: ${(props) => props.theme.console.optionLabelColor};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.filter-option-count {
|
||||
color: ${(props) => props.theme.console.optionCountColor};
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 400;
|
||||
margin-left: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,128 +1,33 @@
|
||||
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
IconFilter,
|
||||
IconChevronDown,
|
||||
IconNetwork
|
||||
} from '@tabler/icons';
|
||||
import {
|
||||
updateNetworkFilter,
|
||||
toggleAllNetworkFilters,
|
||||
setSelectedRequest
|
||||
} from 'providers/ReduxStore/slices/logs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const MethodBadge = ({ method }) => {
|
||||
const getMethodColor = (method) => {
|
||||
switch (method?.toUpperCase()) {
|
||||
case 'GET': return '#10b981';
|
||||
case 'POST': return '#8b5cf6';
|
||||
case 'PUT': return '#f59e0b';
|
||||
case 'DELETE': return '#ef4444';
|
||||
case 'PATCH': return '#06b6d4';
|
||||
case 'HEAD': return '#6b7280';
|
||||
case 'OPTIONS': return '#84cc16';
|
||||
default: return '#6b7280';
|
||||
}
|
||||
};
|
||||
const methodLower = method?.toLowerCase() || 'get';
|
||||
|
||||
return (
|
||||
<span
|
||||
className="method-badge"
|
||||
style={{ backgroundColor: getMethodColor(method) }}
|
||||
>
|
||||
<span className={`method-badge ${methodLower}`}>
|
||||
{method?.toUpperCase() || 'GET'}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusBadge = ({ status, statusCode }) => {
|
||||
const getStatusColor = (code) => {
|
||||
if (code >= 200 && code < 300) return '#10b981';
|
||||
if (code >= 300 && code < 400) return '#f59e0b';
|
||||
if (code >= 400 && code < 500) return '#ef4444';
|
||||
if (code >= 500) return '#dc2626';
|
||||
return '#6b7280';
|
||||
};
|
||||
|
||||
const displayStatus = statusCode || status;
|
||||
|
||||
return (
|
||||
<span
|
||||
className="status-badge"
|
||||
style={{ color: getStatusColor(statusCode) }}
|
||||
>
|
||||
<span className="status-badge">
|
||||
{displayStatus}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggleAll }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
const allFiltersEnabled = Object.values(filters).every((f) => f);
|
||||
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="filter-dropdown" ref={dropdownRef}>
|
||||
<button
|
||||
className="filter-dropdown-trigger"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
title="Filter requests by method"
|
||||
>
|
||||
<IconFilter size={16} strokeWidth={1.5} />
|
||||
<span className="filter-summary">
|
||||
{activeFilters.length === Object.keys(filters).length ? 'All' : `${activeFilters.length}/${Object.keys(filters).length}`}
|
||||
</span>
|
||||
<IconChevronDown size={14} strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="filter-dropdown-menu right">
|
||||
<div className="filter-dropdown-header">
|
||||
<span>Filter by Method</span>
|
||||
<button
|
||||
className="filter-toggle-all"
|
||||
onClick={() => onToggleAll(!allFiltersEnabled)}
|
||||
>
|
||||
{allFiltersEnabled ? 'Hide All' : 'Show All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="filter-dropdown-options">
|
||||
{Object.keys(filters).map((method) => (
|
||||
<label key={method} className="filter-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters[method]}
|
||||
onChange={(e) => onFilterToggle(method, e.target.checked)}
|
||||
/>
|
||||
<div className="filter-option-content">
|
||||
<MethodBadge method={method} />
|
||||
<span className="filter-option-label">{method}</span>
|
||||
<span className="filter-option-count">({requestCounts[method] || 0})</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RequestRow = ({ request, isSelected, onClick }) => {
|
||||
const { data } = request;
|
||||
const { request: req, response: res, timestamp } = data;
|
||||
@@ -241,22 +146,6 @@ const NetworkTab = () => {
|
||||
});
|
||||
}, [allRequests, networkFilters]);
|
||||
|
||||
const requestCounts = useMemo(() => {
|
||||
return allRequests.reduce((counts, request) => {
|
||||
const method = request.data?.request?.method?.toUpperCase() || 'GET';
|
||||
counts[method] = (counts[method] || 0) + 1;
|
||||
return counts;
|
||||
}, {});
|
||||
}, [allRequests]);
|
||||
|
||||
const handleFilterToggle = (method, enabled) => {
|
||||
dispatch(updateNetworkFilter({ method, enabled }));
|
||||
};
|
||||
|
||||
const handleToggleAllFilters = (enabled) => {
|
||||
dispatch(toggleAllNetworkFilters(enabled));
|
||||
};
|
||||
|
||||
const handleRequestClick = (request) => {
|
||||
dispatch(setSelectedRequest(request));
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
padding: 2px 8px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
flex-shrink: 0;
|
||||
@@ -27,7 +27,6 @@ const StyledWrapper = styled.div`
|
||||
gap: 8px;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-weight: 500;
|
||||
|
||||
.request-time {
|
||||
color: ${(props) => props.theme.console.countColor};
|
||||
@@ -66,7 +65,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
@@ -92,7 +91,7 @@ const StyledWrapper = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 16px;
|
||||
padding: 8px;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
@@ -168,10 +167,9 @@ const StyledWrapper = styled.div`
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
|
||||
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
text-transform: uppercase;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
@@ -198,7 +196,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
padding: 2px 8px;
|
||||
vertical-align: top;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -256,10 +254,8 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.response-body-container {
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -267,14 +263,12 @@ const StyledWrapper = styled.div`
|
||||
.w-full.h-full.relative.flex {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
background: ${(props) => props.theme.console.headerBg} !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
div[role="tablist"] {
|
||||
background: ${(props) => props.theme.console.dropdownHeaderBg};
|
||||
padding: 8px 12px;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
display: flex !important;
|
||||
gap: 8px !important;
|
||||
@@ -282,28 +276,17 @@ const StyledWrapper = styled.div`
|
||||
align-items: center !important;
|
||||
min-height: 40px !important;
|
||||
flex-shrink: 0 !important;
|
||||
|
||||
|
||||
> div {
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
font-size: ${(props) => props.theme.font.size.sm} !important;
|
||||
padding: 6px 12px !important;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
white-space: nowrap !important;
|
||||
min-width: auto !important;
|
||||
height: auto !important;
|
||||
line-height: 1.2 !important;
|
||||
font-weight: 500 !important;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
border-color: ${(props) => props.theme.console.buttonHoverBg};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.console.checkboxColor};
|
||||
color: white;
|
||||
@@ -320,7 +303,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.network-logs-container {
|
||||
.network-logs-wrapper {
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
@@ -328,17 +311,17 @@ const StyledWrapper = styled.div`
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
|
||||
.network-logs {
|
||||
.network-logs-container {
|
||||
background: ${(props) => props.theme.console.contentBg} !important;
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
height: 100% !important;
|
||||
max-height: 400px !important;
|
||||
|
||||
pre {
|
||||
padding: 0.5rem !important;
|
||||
|
||||
.network-logs-pre {
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
font-size: ${(props) => props.theme.font.size.xs} !important;
|
||||
line-height: 1.4 !important;
|
||||
padding: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
IconNetwork
|
||||
} from '@tabler/icons';
|
||||
import { clearSelectedRequest } from 'providers/ReduxStore/slices/logs';
|
||||
import QueryResult from 'components/ResponsePane/QueryResult';
|
||||
import QueryResponse from 'components/ResponsePane/QueryResponse/index';
|
||||
import Network from 'components/ResponsePane/Timeline/TimelineItem/Network';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { uuid } from 'utils/common/index';
|
||||
@@ -116,7 +116,7 @@ const ResponseTab = ({ response, request, collection }) => {
|
||||
<h4>Response Body</h4>
|
||||
<div className="response-body-container">
|
||||
{response?.data || response?.dataBuffer ? (
|
||||
<QueryResult
|
||||
<QueryResponse
|
||||
item={{ uid: uuid() }}
|
||||
collection={collection}
|
||||
data={response.data}
|
||||
@@ -141,7 +141,7 @@ const NetworkTab = ({ response }) => {
|
||||
<div className="tab-content">
|
||||
<div className="section">
|
||||
<h4>Network Logs</h4>
|
||||
<div className="network-logs-container">
|
||||
<div className="network-logs-wrapper">
|
||||
{timeline.length > 0 ? (
|
||||
<Network logs={timeline} />
|
||||
) : (
|
||||
|
||||
@@ -13,7 +13,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
padding: 0 8px;
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
flex-shrink: 0;
|
||||
@@ -30,7 +30,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
@@ -38,8 +38,6 @@ const StyledWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
font-weight: 500;
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
@@ -47,9 +45,9 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.console.checkboxColor};
|
||||
border-bottom-color: ${(props) => props.theme.console.checkboxColor};
|
||||
background: ${(props) => props.theme.console.contentBg};
|
||||
color: ${(props) => props.theme.primary.strong};
|
||||
border-bottom-color: ${(props) => props.theme.primary.strong};
|
||||
background: ${(props) => props.theme.background.mantle};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,9 +142,6 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-right: 8px;
|
||||
padding-right: 8px;
|
||||
border-right: 1px solid ${(props) => props.theme.console.border};
|
||||
}
|
||||
|
||||
.action-controls {
|
||||
@@ -159,23 +154,21 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
}
|
||||
|
||||
&.close-button:hover {
|
||||
background: #e81123;
|
||||
color: white;
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,19 +180,17 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
padding: 2px 8px;
|
||||
background: transparent;
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
border: 1px solid ${(props) => props.theme.border.border0};
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.console.buttonHoverBg};
|
||||
color: ${(props) => props.theme.console.buttonHoverColor};
|
||||
border-color: ${(props) => props.theme.console.border};
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
}
|
||||
|
||||
.filter-summary {
|
||||
@@ -232,7 +223,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
padding: 4px 12px;
|
||||
background: ${(props) => props.theme.console.dropdownHeaderBg};
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
@@ -263,7 +254,7 @@ const StyledWrapper = styled.div`
|
||||
.filter-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
@@ -325,20 +316,6 @@ const StyledWrapper = styled.div`
|
||||
.logs-container {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.method-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: flex;
|
||||
|
||||
@@ -5,10 +5,9 @@ import ToolHint from 'components/ToolHint/index';
|
||||
|
||||
const StyledSessionList = styled.div`
|
||||
.session-list-item {
|
||||
padding: 10px 12px;
|
||||
padding: 2px 6px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid ${(props) => props.theme.border || 'rgba(255, 255, 255, 0.05)'};
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@@ -24,7 +23,8 @@ const StyledSessionList = styled.div`
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.sidebarActive || 'rgba(59, 142, 234, 0.12)'};
|
||||
border-left: 2px solid ${(props) => props.theme.brandColor || '#3b8eea'};
|
||||
border-left: 2px solid ${(props) => props.theme.primary.subtle};
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
|
||||
@@ -30,7 +30,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.terminal-sessions-header {
|
||||
padding: 12px;
|
||||
padding: 6px 8px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
@@ -183,6 +183,29 @@ const openTerminalIntoContainer = async (container, sessionId) => {
|
||||
}
|
||||
};
|
||||
|
||||
let fitFrameRef;
|
||||
const fitTerminal = (activeSessionId, container) => {
|
||||
if (!container) return;
|
||||
|
||||
const instance = terminalInstances.get(activeSessionId);
|
||||
if (!instance?.fitAddon) return;
|
||||
|
||||
if (fitFrameRef) {
|
||||
cancelAnimationFrame(fitFrameRef);
|
||||
}
|
||||
|
||||
fitFrameRef = requestAnimationFrame(() => {
|
||||
fitFrameRef = null;
|
||||
|
||||
// Avoid fitting when hidden/0-sized (common during tab switches/layout transitions)
|
||||
if (container.offsetWidth === 0 || container.offsetHeight === 0) return;
|
||||
|
||||
try {
|
||||
instance.fitAddon.fit();
|
||||
} catch (e) {}
|
||||
});
|
||||
};
|
||||
|
||||
const TerminalTab = () => {
|
||||
const terminalRef = useRef(null);
|
||||
const [sessions, setSessions] = useState([]);
|
||||
@@ -223,22 +246,25 @@ const TerminalTab = () => {
|
||||
}, []);
|
||||
|
||||
// Create new terminal session
|
||||
const createNewSession = useCallback(async (cwd = null) => {
|
||||
if (!window.ipcRenderer) return null;
|
||||
const createNewSession = useCallback(
|
||||
async (cwd = null) => {
|
||||
if (!window.ipcRenderer) return null;
|
||||
|
||||
try {
|
||||
const options = cwd ? { cwd } : {};
|
||||
const newSessionId = await window.ipcRenderer.invoke('terminal:create', options);
|
||||
if (newSessionId) {
|
||||
await loadSessions(newSessionId);
|
||||
setActiveSessionId(newSessionId);
|
||||
return newSessionId;
|
||||
try {
|
||||
const options = cwd ? { cwd } : {};
|
||||
const newSessionId = await window.ipcRenderer.invoke('terminal:create', options);
|
||||
if (newSessionId) {
|
||||
await loadSessions(newSessionId);
|
||||
setActiveSessionId(newSessionId);
|
||||
return newSessionId;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create terminal session:', err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create terminal session:', err);
|
||||
}
|
||||
return null;
|
||||
}, [loadSessions]);
|
||||
return null;
|
||||
},
|
||||
[loadSessions]
|
||||
);
|
||||
|
||||
// Listen for requests to open terminal at specific CWD
|
||||
useEffect(() => {
|
||||
@@ -339,31 +365,17 @@ const TerminalTab = () => {
|
||||
|
||||
if (mounted) {
|
||||
const instance = terminalInstances.get(activeSessionId);
|
||||
if (instance && instance.fitAddon) {
|
||||
const onResize = () => {
|
||||
try {
|
||||
instance.fitAddon.fit();
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
// Initial resize
|
||||
setTimeout(() => {
|
||||
try {
|
||||
instance.fitAddon.fit();
|
||||
const { cols, rows } = instance.terminal;
|
||||
if (cols && rows && window.ipcRenderer) {
|
||||
window.ipcRenderer.send('terminal:resize', activeSessionId, { cols, rows });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to perform initial resize:', err);
|
||||
if (instance) {
|
||||
try {
|
||||
const { cols, rows } = instance.terminal;
|
||||
if (cols && rows && window.ipcRenderer) {
|
||||
window.ipcRenderer.send('terminal:resize', activeSessionId, { cols, rows });
|
||||
}
|
||||
}, 100);
|
||||
} catch (err) {
|
||||
console.warn('Failed to perform initial resize:', err);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', onResize);
|
||||
|
||||
// Park terminal element when switching sessions
|
||||
if (instance.terminal && instance.terminal.element) {
|
||||
const host = ensureParkingHost();
|
||||
@@ -386,6 +398,18 @@ const TerminalTab = () => {
|
||||
};
|
||||
}, [activeSessionId]);
|
||||
|
||||
const onSessionMount = useCallback(
|
||||
(node) => {
|
||||
if (!node) return;
|
||||
terminalRef.current = node;
|
||||
fitTerminal(activeSessionId, node);
|
||||
const ro = new ResizeObserver(() => fitTerminal(activeSessionId, node));
|
||||
ro.observe(node.parentNode);
|
||||
return () => ro.disconnect();
|
||||
},
|
||||
[activeSessionId]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="terminal-content">
|
||||
@@ -405,13 +429,9 @@ const TerminalTab = () => {
|
||||
</div>
|
||||
<div className="terminal-sessions-list">
|
||||
{isLoading ? (
|
||||
<div style={{ padding: '12px', color: '#888', fontSize: '13px' }}>
|
||||
Loading sessions...
|
||||
</div>
|
||||
<div style={{ padding: '12px', color: '#888', fontSize: '13px' }}>Loading sessions...</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div style={{ padding: '12px', color: '#888', fontSize: '13px' }}>
|
||||
No active sessions
|
||||
</div>
|
||||
<div style={{ padding: '12px', color: '#888', fontSize: '13px' }}>No active sessions</div>
|
||||
) : (
|
||||
<SessionList
|
||||
sessions={sessions}
|
||||
@@ -432,7 +452,7 @@ const TerminalTab = () => {
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={terminalRef}
|
||||
ref={onSessionMount}
|
||||
className="terminal-container"
|
||||
style={{
|
||||
height: '100%',
|
||||
|
||||
@@ -182,19 +182,6 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
|
||||
const allFiltersEnabled = Object.values(filters).every((f) => f);
|
||||
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
|
||||
|
||||
const getMethodColor = (method) => {
|
||||
switch (method?.toUpperCase()) {
|
||||
case 'GET': return '#10b981';
|
||||
case 'POST': return '#8b5cf6';
|
||||
case 'PUT': return '#f59e0b';
|
||||
case 'DELETE': return '#ef4444';
|
||||
case 'PATCH': return '#06b6d4';
|
||||
case 'HEAD': return '#6b7280';
|
||||
case 'OPTIONS': return '#84cc16';
|
||||
default: return '#6b7280';
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
@@ -241,9 +228,6 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
|
||||
onChange={(e) => onFilterToggle(method, e.target.checked)}
|
||||
/>
|
||||
<div className="filter-option-content">
|
||||
<span className="method-badge" style={{ backgroundColor: getMethodColor(method) }}>
|
||||
{method}
|
||||
</span>
|
||||
<span className="filter-option-label">{method}</span>
|
||||
<span className="filter-option-count">({requestCounts[method] || 0})</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { darken } from 'polished';
|
||||
import Console from './Console';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const MIN_DEVTOOLS_HEIGHT = 150;
|
||||
const MAX_DEVTOOLS_HEIGHT = window.innerHeight * 0.7;
|
||||
@@ -10,6 +12,9 @@ const Devtools = ({ mainSectionRef }) => {
|
||||
const isDevtoolsOpen = useSelector((state) => state.logs.isConsoleOpen);
|
||||
const [devtoolsHeight, setDevtoolsHeight] = useState(DEFAULT_DEVTOOLS_HEIGHT);
|
||||
const [isResizingDevtools, setIsResizingDevtools] = useState(false);
|
||||
const { theme } = useTheme();
|
||||
|
||||
const dragHandleColor = useMemo(() => darken(0.1, theme.primary.subtle), [theme.primary.subtle]);
|
||||
|
||||
const handleDevtoolsResizeStart = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
@@ -68,15 +73,15 @@ const Devtools = ({ mainSectionRef }) => {
|
||||
<div
|
||||
onMouseDown={handleDevtoolsResizeStart}
|
||||
style={{
|
||||
height: '4px',
|
||||
height: '2px',
|
||||
cursor: 'row-resize',
|
||||
backgroundColor: isResizingDevtools ? '#0078d4' : 'transparent',
|
||||
backgroundColor: isResizingDevtools ? dragHandleColor : 'transparent',
|
||||
transition: 'background-color 0.2s ease',
|
||||
zIndex: 20,
|
||||
position: 'relative'
|
||||
}}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = '#0078d4'}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = isResizingDevtools ? '#0078d4' : 'transparent'}
|
||||
onMouseEnter={(e) => e.target.style.backgroundColor = dragHandleColor}
|
||||
onMouseLeave={(e) => e.target.style.backgroundColor = isResizingDevtools ? dragHandleColor : 'transparent'}
|
||||
/>
|
||||
<div style={{ height: `${devtoolsHeight}px`, overflow: 'hidden', position: 'relative' }}>
|
||||
<Console />
|
||||
|
||||
@@ -1,115 +1,177 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.dropdown-toggle {
|
||||
&:hover {
|
||||
color: black;
|
||||
min-width: 160px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
background-color: ${(props) => props.theme.dropdown.bg};
|
||||
${(props) =>
|
||||
props.theme.dropdown.shadow && props.theme.dropdown.shadow !== 'none'
|
||||
? `box-shadow: ${props.theme.dropdown.shadow};`
|
||||
: ''}
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
${(props) =>
|
||||
props.theme.dropdown.border && props.theme.dropdown.border !== 'none'
|
||||
? `border: 1px solid ${props.theme.dropdown.border};`
|
||||
: ''}
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
max-width: unset !important;
|
||||
padding: 0.25rem;
|
||||
|
||||
[role="menu"] {
|
||||
outline: none;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box {
|
||||
min-width: 160px;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
.label-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.375rem 0.625rem 0.25rem 0.625rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.025em;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
background-color: ${(props) => props.theme.dropdown.bg};
|
||||
box-shadow: ${(props) => props.theme.shadow.sm};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
max-width: unset !important;
|
||||
padding: 0.25rem;
|
||||
opacity: 0.6;
|
||||
margin-top: 0.25rem;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-content {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.275rem 0.625rem;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
margin: 0.0625rem 0;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
.label-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.375rem 0.625rem 0.25rem 0.625rem;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
opacity: 0.6;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.275rem 0.625rem;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
margin: 0.0625rem 0;
|
||||
font-size: 0.8125rem;
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.colors.text.yellow} !important;
|
||||
.dropdown-icon {
|
||||
color: ${(props) => props.theme.colors.text.yellow} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${(props) => props.theme.dropdown.iconColor};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.delete-item {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
.dropdown-icon {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => {
|
||||
const hex = theme.colors.text.danger.replace('#', '');
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, 0.04)`; // 4% opacity
|
||||
}} !important;
|
||||
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.border-top {
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
|
||||
margin-top: 0.25rem;
|
||||
padding-top: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-separator {
|
||||
height: 1px;
|
||||
background-color: ${(props) => props.theme.dropdown.separator};
|
||||
margin: 0.25rem 0;
|
||||
&.active {
|
||||
color: ${(props) => props.theme.colors.text.yellow} !important;
|
||||
.dropdown-icon {
|
||||
color: ${(props) => props.theme.colors.text.yellow} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${(props) => props.theme.dropdown.iconColor};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.dropdown-right-section {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&.selected-focused:not(:disabled):not(.disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:focus-visible:not(:disabled):not(.disabled) {
|
||||
outline: none;
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.delete-item {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
.dropdown-icon {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => {
|
||||
const hex = theme.colors.text.danger.replace('#', '');
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, 0.04)`; // 4% opacity
|
||||
}} !important;
|
||||
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.border-top {
|
||||
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
|
||||
margin-top: 0.25rem;
|
||||
padding-top: 0.375rem;
|
||||
}
|
||||
|
||||
&.dropdown-item-select {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
/* Focused state - applied during keyboard navigation */
|
||||
&.dropdown-item-focused {
|
||||
background-color: ${({ theme }) => theme.dropdown.hoverBg};
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Active/selected state - applied to the currently selected item */
|
||||
&.dropdown-item-active {
|
||||
color: ${({ theme }) => theme.dropdown.selectedColor} !important;
|
||||
background-color: ${({ theme }) => rgba(theme.dropdown.selectedColor, 0.07)} !important;
|
||||
.dropdown-icon {
|
||||
color: ${({ theme }) => theme.dropdown.selectedColor} !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.dropdown.selectedColor} !important;
|
||||
background-color: ${({ theme }) => rgba(theme.dropdown.selectedColor, 0.07)} !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Combined state - when active item is also focused */
|
||||
&.dropdown-item-active.dropdown-item-focused {
|
||||
background-color: ${({ theme }) => rgba(theme.dropdown.selectedColor, 0.07)} !important;
|
||||
}
|
||||
|
||||
/* Focus visible for accessibility */
|
||||
&:focus-visible {
|
||||
outline: 2px solid ${({ theme }) => theme.dropdown.focusRing};
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-separator {
|
||||
height: 1px;
|
||||
background-color: ${(props) => props.theme.dropdown.separator};
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -2,25 +2,27 @@ import React from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent, visible, ...props }) => {
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent, visible, appendTo, ...props }) => {
|
||||
// When in controlled mode (visible prop is provided), don't use trigger prop
|
||||
const tippyProps = visible !== undefined
|
||||
? { ...props, visible, interactive: true, appendTo: 'parent' }
|
||||
: { ...props, trigger: 'click', interactive: true, appendTo: 'parent' };
|
||||
? { ...props, visible, interactive: true, appendTo: appendTo || 'parent' }
|
||||
: { ...props, trigger: 'click', interactive: true, appendTo: appendTo || 'parent' };
|
||||
|
||||
return (
|
||||
<StyledWrapper className="dropdown" transparent={transparent}>
|
||||
<Tippy
|
||||
content={children}
|
||||
placement={placement || 'bottom-end'}
|
||||
animation={false}
|
||||
arrow={false}
|
||||
onCreate={onCreate}
|
||||
{...tippyProps}
|
||||
>
|
||||
{icon}
|
||||
</Tippy>
|
||||
</StyledWrapper>
|
||||
<Tippy
|
||||
render={(attrs) => (
|
||||
<StyledWrapper className="tippy-box dropdown" transparent={transparent} tabIndex={-1} {...attrs}>
|
||||
{children}
|
||||
</StyledWrapper>
|
||||
)}
|
||||
placement={placement || 'bottom-end'}
|
||||
animation={false}
|
||||
arrow={false}
|
||||
onCreate={onCreate}
|
||||
{...tippyProps}
|
||||
>
|
||||
{icon}
|
||||
</Tippy>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
152
packages/bruno-app/src/components/EditableTable/StyledWrapper.js
Normal file
152
packages/bruno-app/src/components/EditableTable/StyledWrapper.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.table-container {
|
||||
overflow-y: auto;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
border: solid 1px ${(props) => props.theme.border.border0};
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color} !important;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
user-select: none;
|
||||
|
||||
border: none !important;
|
||||
|
||||
td {
|
||||
padding: 5px 10px !important;
|
||||
border-top: none !important;
|
||||
border-left: none !important;
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
vertical-align: middle;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-checkbox thead td:nth-child(1) {
|
||||
width: 25px !important;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
transition: background 0.1s ease;
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 1px 10px !important;
|
||||
border-top: none !important;
|
||||
border-left: none !important;
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
vertical-align: middle;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-checkbox tbody td:nth-child(1) {
|
||||
width: 25px;
|
||||
border-right: none;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
|
||||
input[type='checkbox'] {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-mod {
|
||||
max-width: 200px !important;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
outline: none !important;
|
||||
background-color: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: ${(props) => props.theme.colors.accent};
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s ease, background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
.icon-grip,
|
||||
.icon-minus {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
background-color: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
option {
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
325
packages/bruno-app/src/components/EditableTable/index.js
Normal file
325
packages/bruno-app/src/components/EditableTable/index.js
Normal file
@@ -0,0 +1,325 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { IconTrash, IconAlertCircle, IconGripVertical, IconMinusVertical } from '@tabler/icons';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { uuid } from 'utils/common';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const EditableTable = ({
|
||||
columns,
|
||||
rows,
|
||||
onChange,
|
||||
defaultRow,
|
||||
getRowError,
|
||||
showCheckbox = true,
|
||||
showDelete = true,
|
||||
disableCheckbox = false,
|
||||
checkboxLabel = '',
|
||||
checkboxKey = 'enabled',
|
||||
reorderable = false,
|
||||
onReorder,
|
||||
showAddRow = true,
|
||||
testId = 'editable-table'
|
||||
}) => {
|
||||
const tableRef = useRef(null);
|
||||
const emptyRowUidRef = useRef(null);
|
||||
const [hoveredRow, setHoveredRow] = useState(null);
|
||||
const [dragStart, setDragStart] = useState(null);
|
||||
|
||||
const createEmptyRow = useCallback(() => {
|
||||
const newUid = uuid();
|
||||
emptyRowUidRef.current = newUid;
|
||||
return {
|
||||
uid: newUid,
|
||||
[checkboxKey]: true,
|
||||
...defaultRow
|
||||
};
|
||||
}, [defaultRow, checkboxKey]);
|
||||
|
||||
const rowsWithEmpty = useMemo(() => {
|
||||
if (!showAddRow) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
return [createEmptyRow()];
|
||||
}
|
||||
|
||||
const lastRow = rows[rows.length - 1];
|
||||
const keyColumn = columns.find((col) => col.isKeyField);
|
||||
|
||||
if (keyColumn) {
|
||||
const lastRowKeyValue = keyColumn.getValue ? keyColumn.getValue(lastRow) : lastRow[keyColumn.key];
|
||||
const isLastRowEmpty = !lastRowKeyValue || (typeof lastRowKeyValue === 'string' && lastRowKeyValue.trim() === '');
|
||||
|
||||
if (isLastRowEmpty) {
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
if (!emptyRowUidRef.current || rows.some((r) => r.uid === emptyRowUidRef.current)) {
|
||||
emptyRowUidRef.current = uuid();
|
||||
}
|
||||
|
||||
return [...rows, {
|
||||
uid: emptyRowUidRef.current,
|
||||
[checkboxKey]: true,
|
||||
...defaultRow
|
||||
}];
|
||||
}, [rows, columns, defaultRow, checkboxKey, createEmptyRow, showAddRow]);
|
||||
|
||||
const isEmptyRow = useCallback((row) => {
|
||||
const keyColumn = columns.find((col) => col.isKeyField);
|
||||
if (!keyColumn) return false;
|
||||
|
||||
const value = keyColumn.getValue ? keyColumn.getValue(row) : row[keyColumn.key];
|
||||
return !value || (typeof value === 'string' && value.trim() === '');
|
||||
}, [columns]);
|
||||
|
||||
const isLastEmptyRow = useCallback((row, index) => {
|
||||
if (!showAddRow) return false;
|
||||
return index === rowsWithEmpty.length - 1 && isEmptyRow(row);
|
||||
}, [rowsWithEmpty.length, isEmptyRow, showAddRow]);
|
||||
|
||||
const handleValueChange = useCallback((rowUid, key, value) => {
|
||||
const rowIndex = rowsWithEmpty.findIndex((r) => r.uid === rowUid);
|
||||
if (rowIndex === -1) return;
|
||||
|
||||
const currentRow = rowsWithEmpty[rowIndex];
|
||||
const isLast = rowIndex === rowsWithEmpty.length - 1;
|
||||
const wasEmpty = isEmptyRow(currentRow);
|
||||
|
||||
const keyColumn = columns.find((col) => col.isKeyField);
|
||||
const isKeyFieldChange = keyColumn && keyColumn.key === key;
|
||||
|
||||
let updatedRows = rowsWithEmpty.map((row) => {
|
||||
if (row.uid === rowUid) {
|
||||
return { ...row, [key]: value };
|
||||
}
|
||||
return row;
|
||||
});
|
||||
|
||||
// Only add a new empty row when the key field is filled
|
||||
if (showAddRow && isLast && wasEmpty && isKeyFieldChange && value && value.trim() !== '') {
|
||||
emptyRowUidRef.current = uuid();
|
||||
updatedRows.push({
|
||||
uid: emptyRowUidRef.current,
|
||||
[checkboxKey]: true,
|
||||
...defaultRow
|
||||
});
|
||||
}
|
||||
|
||||
const hasAnyValue = (row) => {
|
||||
for (const col of columns) {
|
||||
const val = col.getValue ? col.getValue(row) : row[col.key];
|
||||
const defaultVal = defaultRow[col.key];
|
||||
if (val && val !== defaultVal && (typeof val !== 'string' || val.trim() !== '')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const result = updatedRows.filter((row, i) => {
|
||||
if (showAddRow && i === updatedRows.length - 1) {
|
||||
return hasAnyValue(row);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
onChange(result);
|
||||
}, [rowsWithEmpty, columns, onChange, checkboxKey, defaultRow, isEmptyRow, showAddRow]);
|
||||
|
||||
const handleCheckboxChange = useCallback((rowUid, checked) => {
|
||||
handleValueChange(rowUid, checkboxKey, checked);
|
||||
}, [handleValueChange, checkboxKey]);
|
||||
|
||||
const handleRemoveRow = useCallback((rowUid) => {
|
||||
const filteredRows = rows.filter((row) => row.uid !== rowUid);
|
||||
onChange(filteredRows);
|
||||
}, [rows, onChange]);
|
||||
|
||||
const getColumnWidth = useCallback((column) => {
|
||||
if (column.width) return column.width;
|
||||
return 'auto';
|
||||
}, []);
|
||||
|
||||
const handleDragStart = useCallback((e, index) => {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', index);
|
||||
setDragStart(index);
|
||||
}, []);
|
||||
|
||||
const handleDragOver = useCallback((e, index) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
setHoveredRow(index);
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback((e, toIndex) => {
|
||||
e.preventDefault();
|
||||
const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10);
|
||||
if (fromIndex !== toIndex && onReorder) {
|
||||
const reorderableRows = showAddRow ? rowsWithEmpty.slice(0, -1) : rowsWithEmpty;
|
||||
const updatedOrder = [...reorderableRows];
|
||||
const [movedRow] = updatedOrder.splice(fromIndex, 1);
|
||||
updatedOrder.splice(toIndex, 0, movedRow);
|
||||
onReorder({ updateReorderedItem: updatedOrder.map((row) => row.uid) });
|
||||
}
|
||||
setDragStart(null);
|
||||
setHoveredRow(null);
|
||||
}, [onReorder, rowsWithEmpty, showAddRow]);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
setDragStart(null);
|
||||
setHoveredRow(null);
|
||||
}, []);
|
||||
|
||||
const renderCell = useCallback((column, row, rowIndex) => {
|
||||
const isEmpty = isLastEmptyRow(row, rowIndex);
|
||||
const value = column.getValue ? column.getValue(row) : row[column.key];
|
||||
const error = getRowError?.(row, rowIndex, column.key);
|
||||
|
||||
if (column.render) {
|
||||
return column.render({
|
||||
row,
|
||||
value,
|
||||
rowIndex,
|
||||
isLastEmptyRow: isEmpty,
|
||||
onChange: (newValue) => handleValueChange(row.uid, column.key, newValue),
|
||||
error
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
value={value || ''}
|
||||
readOnly={column.readOnly}
|
||||
placeholder={isEmpty ? column.placeholder || column.name : ''}
|
||||
onChange={(e) => handleValueChange(row.uid, column.key, e.target.value)}
|
||||
/>
|
||||
{error && !isEmpty && (
|
||||
<span>
|
||||
<IconAlertCircle
|
||||
data-tooltip-id={`error-${row.uid}-${column.key}`}
|
||||
className="text-red-600 cursor-pointer"
|
||||
size={20}
|
||||
/>
|
||||
<Tooltip
|
||||
className="tooltip-mod"
|
||||
id={`error-${row.uid}-${column.key}`}
|
||||
html={error}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [isLastEmptyRow, getRowError, handleValueChange]);
|
||||
|
||||
const reorderableRowCount = showAddRow ? rowsWithEmpty.length - 1 : rowsWithEmpty.length;
|
||||
|
||||
return (
|
||||
<StyledWrapper className={showCheckbox ? 'has-checkbox' : 'no-checkbox'}>
|
||||
<div className="table-container" ref={tableRef} data-testid={testId}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{showCheckbox && (
|
||||
<td className="text-center">{checkboxLabel}</td>
|
||||
)}
|
||||
{columns.map((column) => (
|
||||
<td
|
||||
key={column.key}
|
||||
style={{ width: getColumnWidth(column) }}
|
||||
>
|
||||
{column.name}
|
||||
</td>
|
||||
))}
|
||||
{showDelete && (
|
||||
<td style={{ width: '60px' }}></td>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rowsWithEmpty.map((row, rowIndex) => {
|
||||
const isEmpty = isLastEmptyRow(row, rowIndex);
|
||||
const canDrag = reorderable && !isEmpty && rowIndex < reorderableRowCount;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={row.uid}
|
||||
draggable={canDrag}
|
||||
onDragStart={canDrag ? (e) => handleDragStart(e, rowIndex) : undefined}
|
||||
onDragOver={canDrag ? (e) => handleDragOver(e, rowIndex) : undefined}
|
||||
onDrop={canDrag ? (e) => handleDrop(e, rowIndex) : undefined}
|
||||
onDragEnd={canDrag ? handleDragEnd : undefined}
|
||||
onMouseEnter={() => setHoveredRow(rowIndex)}
|
||||
onMouseLeave={() => setHoveredRow(null)}
|
||||
>
|
||||
{showCheckbox && (
|
||||
<td className="text-center relative">
|
||||
{reorderable && canDrag && (
|
||||
<div
|
||||
draggable
|
||||
className="drag-handle group absolute z-10 left-[-8px] top-1/2 -translate-y-1/2 p-1 cursor-grab"
|
||||
>
|
||||
{hoveredRow === rowIndex && (
|
||||
<>
|
||||
<IconGripVertical
|
||||
size={14}
|
||||
className="icon-grip hidden group-hover:block"
|
||||
/>
|
||||
<IconMinusVertical
|
||||
size={14}
|
||||
className="icon-minus block group-hover:hidden"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isEmpty && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
data-testid="column-checkbox"
|
||||
checked={row[checkboxKey] ?? true}
|
||||
disabled={disableCheckbox}
|
||||
onChange={(e) => handleCheckboxChange(row.uid, e.target.checked)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
{columns.map((column) => (
|
||||
<td key={column.key} data-testid={`column-${column.key}`}>
|
||||
{renderCell(column, row, rowIndex)}
|
||||
</td>
|
||||
))}
|
||||
{showDelete && (
|
||||
<td>
|
||||
{!isEmpty && (
|
||||
<button
|
||||
data-testid="column-delete"
|
||||
onClick={() => handleRemoveRow(row.uid)}
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={18} />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditableTable;
|
||||
@@ -6,6 +6,7 @@ import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ExportEnvironmentModal = ({ onClose, environments = [], environmentType }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -242,22 +243,26 @@ const ExportEnvironmentModal = ({ onClose, environments = [], environmentType })
|
||||
|
||||
{/* Export Actions */}
|
||||
<div className="flex justify-end gap-2 mt-4 pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn-sm btn-cancel mt-2 flex items-center"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onClick={onClose}
|
||||
disabled={isExporting}
|
||||
className="mt-2 mr-2"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary mt-2 flex items-center"
|
||||
size="sm"
|
||||
onClick={handleExport}
|
||||
disabled={isExporting || selectedCount === 0}
|
||||
className="mt-2"
|
||||
>
|
||||
{isExporting ? 'Exporting...' : `Export ${selectedCount || ''} Environment${selectedCount !== 1 ? 's' : ''}`}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import Portal from 'components/Portal';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ConfirmCloseEnvironment = ({ onCancel, onCloseWithoutSave, onSaveAndClose, isGlobal }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="md"
|
||||
title="Unsaved changes"
|
||||
disableEscapeKey={true}
|
||||
disableCloseOnOutsideClick={true}
|
||||
closeModalFadeTimeout={150}
|
||||
handleCancel={onCancel}
|
||||
hideFooter={true}
|
||||
>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-medium">Hold on...</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
You have unsaved changes in {isGlobal ? 'global' : 'collection'} environment settings.
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<Button color="danger" onClick={onCloseWithoutSave}>
|
||||
Don't Save
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" color="secondary" variant="ghost" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onSaveAndClose}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmCloseEnvironment;
|
||||
@@ -33,7 +33,7 @@ const EnvironmentListContent = ({
|
||||
{environments.map((env) => (
|
||||
<div
|
||||
key={env.uid}
|
||||
className={`dropdown-item ${env.uid === activeEnvironmentUid ? 'active' : ''}`}
|
||||
className={`dropdown-item ${env.uid === activeEnvironmentUid ? 'dropdown-item-active' : ''}`}
|
||||
onClick={() => onEnvironmentSelect(env)}
|
||||
data-tooltip-content={env.name}
|
||||
data-tooltip-hidden={env.name?.length < 90}
|
||||
|
||||
@@ -2,46 +2,55 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-environment {
|
||||
border-radius: 0.9375rem;
|
||||
padding: 0.25rem 0.5rem 0.25rem 0.75rem;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
padding: 0.25rem 0.3rem 0.25rem 0.5rem;
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid ${(props) => props.theme.dropdown.selectedColor};
|
||||
background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.bg};
|
||||
border: 1px solid ${(props) => props.theme.app.collection.toolbar.environmentSelector.border};
|
||||
line-height: 1rem;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBorder};
|
||||
background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBg};
|
||||
}
|
||||
|
||||
.caret {
|
||||
margin-left: 0.25rem;
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140, 140, 140);
|
||||
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret};
|
||||
fill: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret};
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.env-icon {
|
||||
margin-right: 0.25rem;
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.icon};
|
||||
}
|
||||
|
||||
.env-text {
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.text};
|
||||
display: block;
|
||||
}
|
||||
|
||||
.env-separator {
|
||||
color: #8c8c8c;
|
||||
margin: 0 0.25rem;
|
||||
opacity: 0.7;
|
||||
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.separator};
|
||||
margin: 0 0.35rem;
|
||||
}
|
||||
|
||||
.env-text-inactive {
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
opacity: 0.7;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
}
|
||||
|
||||
&.no-environments {
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border: 1px solid transparent;
|
||||
color: ${(props) => props.theme.dropdown.secondaryText};
|
||||
color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.text};
|
||||
background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.bg};
|
||||
border: 1px dashed ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.border};
|
||||
|
||||
&:hover {
|
||||
border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.hoverBorder};
|
||||
background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.hoverBg};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,35 +65,6 @@ const Wrapper = styled.div`
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tippy-box .tippy-content {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.6rem;
|
||||
cursor: pointer;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: ${(props) => props.theme.dropdown.selectedBg};
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
}
|
||||
|
||||
&.no-environment {
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.configure-button {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -100,7 +80,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
button {
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -110,8 +90,8 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
color: var(--color-tab-inactive);
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
.tab-content-wrapper {
|
||||
position: relative;
|
||||
@@ -161,7 +141,7 @@ const Wrapper = styled.div`
|
||||
min-height: 12.5rem;
|
||||
|
||||
h3 {
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
@@ -169,7 +149,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
p {
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
opacity: 0.75;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
line-height: 1.5;
|
||||
@@ -185,9 +165,9 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.space-y-2 > button {
|
||||
border: 0.0625rem solid ${(props) => props.theme.dropdown.primaryText};
|
||||
border: 0.0625rem solid ${(props) => props.theme.text};
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
width: 100%;
|
||||
@@ -215,7 +195,7 @@ const Wrapper = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
@@ -223,7 +203,7 @@ const Wrapper = styled.div`
|
||||
|
||||
svg {
|
||||
margin: 0 auto 1rem auto;
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
color: ${(props) => props.theme.text};
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import React, { useMemo, useState, useRef, forwardRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { IconWorld, IconDatabase, IconCaretDown, IconSettings, IconPlus, IconDownload } from '@tabler/icons';
|
||||
import { IconWorld, IconDatabase, IconCaretDown } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { selectGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import toast from 'react-hot-toast';
|
||||
import EnvironmentListContent from './EnvironmentListContent/index';
|
||||
import EnvironmentSettings from '../EnvironmentSettings';
|
||||
import GlobalEnvironmentSettings from 'components/GlobalEnvironments/EnvironmentSettings';
|
||||
import CreateEnvironment from '../EnvironmentSettings/CreateEnvironment';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import CreateGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment';
|
||||
import CreateGlobalEnvironment from 'components/WorkspaceHome/WorkspaceEnvironments/CreateEnvironment';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@@ -20,8 +18,6 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const [activeTab, setActiveTab] = useState('collection');
|
||||
const [showGlobalSettings, setShowGlobalSettings] = useState(false);
|
||||
const [showCollectionSettings, setShowCollectionSettings] = useState(false);
|
||||
const [showCreateGlobalModal, setShowCreateGlobalModal] = useState(false);
|
||||
const [showImportGlobalModal, setShowImportGlobalModal] = useState(false);
|
||||
const [showCreateCollectionModal, setShowCreateCollectionModal] = useState(false);
|
||||
@@ -75,13 +71,24 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Settings handler
|
||||
// Settings handler - opens environment settings tab
|
||||
const handleSettingsClick = () => {
|
||||
if (activeTab === 'collection') {
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(true));
|
||||
setShowCollectionSettings(true);
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'environment-settings'
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setShowGlobalSettings(true);
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-global-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'global-environment-settings'
|
||||
})
|
||||
);
|
||||
}
|
||||
dropdownTippyRef.current.hide();
|
||||
};
|
||||
@@ -106,13 +113,6 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
};
|
||||
|
||||
// Modal handlers
|
||||
const handleCloseSettings = () => {
|
||||
setShowGlobalSettings(false);
|
||||
setShowCollectionSettings(false);
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(false));
|
||||
};
|
||||
|
||||
// Calculate dropdown width based on the longest environment name.
|
||||
// To prevent resizing while switching between collection and global environments.
|
||||
const dropdownWidth = useMemo(() => {
|
||||
@@ -164,7 +164,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="env-text-inactive max-w-36 truncate no-wrap">No environments</span>
|
||||
<span className="env-text-inactive max-w-36 truncate no-wrap">No Environment</span>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -176,7 +176,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
data-testid="environment-selector-trigger"
|
||||
>
|
||||
{displayContent}
|
||||
<IconCaretDown className="caret" size={14} strokeWidth={2} />
|
||||
<IconCaretDown className="caret flex items-center justify-center" size={12} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -186,7 +186,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
<div className="environment-selector flex align-center cursor-pointer">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
{/* Tab Headers */}
|
||||
<div className="tab-header flex p-[0.75rem]">
|
||||
<div className="tab-header flex pt-3 pb-2 px-3">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
@@ -219,23 +219,17 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
{/* Modals - Rendered outside dropdown to avoid conflicts */}
|
||||
{showGlobalSettings && (
|
||||
<GlobalEnvironmentSettings
|
||||
globalEnvironments={globalEnvironments}
|
||||
collection={collection}
|
||||
activeGlobalEnvironmentUid={activeGlobalEnvironmentUid}
|
||||
onClose={handleCloseSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCollectionSettings && <EnvironmentSettings collection={collection} onClose={handleCloseSettings} />}
|
||||
|
||||
{showCreateGlobalModal && (
|
||||
<CreateGlobalEnvironment
|
||||
onClose={() => setShowCreateGlobalModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
setShowGlobalSettings(true);
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-global-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'global-environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -245,7 +239,13 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
type="global"
|
||||
onClose={() => setShowImportGlobalModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
setShowGlobalSettings(true);
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-global-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'global-environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -255,7 +255,13 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
collection={collection}
|
||||
onClose={() => setShowCreateCollectionModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
setShowCollectionSettings(true);
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -266,7 +272,13 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
collection={collection}
|
||||
onClose={() => setShowImportCollectionModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
setShowCollectionSettings(true);
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -26,6 +26,7 @@ const DeleteEnvironment = ({ onClose, environment, collection }) => {
|
||||
confirmText="Delete"
|
||||
handleConfirm={onConfirm}
|
||||
handleCancel={onClose}
|
||||
confirmButtonColor="danger"
|
||||
>
|
||||
Are you sure you want to delete <span className="font-medium">{environment.name}</span> ?
|
||||
</Modal>
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
const ConfirmSwitchEnv = ({ onCancel }) => {
|
||||
return createPortal(
|
||||
<Modal
|
||||
size="md"
|
||||
title="Unsaved changes"
|
||||
confirmText="Save and Close"
|
||||
cancelText="Close without saving"
|
||||
disableEscapeKey={true}
|
||||
disableCloseOnOutsideClick={true}
|
||||
closeModalFadeTimeout={150}
|
||||
handleCancel={onCancel}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
hideFooter={true}
|
||||
>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-medium">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">You have unsaved changes in this environment.</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCancel}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</Modal>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmSwitchEnv;
|
||||
@@ -1,65 +1,166 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.table-container {
|
||||
overflow-y: auto;
|
||||
border-radius: 8px;
|
||||
border: solid 1px ${(props) => props.theme.border.border0};
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 500;
|
||||
table-layout: fixed;
|
||||
font-size: 12px;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
|
||||
padding: 4px 10px;
|
||||
vertical-align: middle;
|
||||
padding: 2px 10px;
|
||||
|
||||
&:nth-child(1),
|
||||
&:nth-child(1) {
|
||||
width: 25px;
|
||||
border-right: none;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
width: 70px;
|
||||
width: 80px;
|
||||
}
|
||||
&:nth-child(5) {
|
||||
width: 40px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 25%;
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
color: ${(props) => props.theme.table.thead.color} !important;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
user-select: none;
|
||||
}
|
||||
thead td {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-param {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
td {
|
||||
padding: 5px 10px !important;
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
transition: background 0.1s ease;
|
||||
|
||||
&:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom: solid 1px ${(props) => props.theme.border.border0};
|
||||
border-right: solid 1px ${(props) => props.theme.border.border0};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-mod {
|
||||
font-size: ${(props) => props.theme.font.size.xs} !important;
|
||||
width: 150px !important;
|
||||
max-width: 200px !important;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
border: 1px solid transparent;
|
||||
outline: none !important;
|
||||
background-color: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
accent-color: ${(props) => props.theme.colors.accent};
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding: 12px 2px;
|
||||
background: ${(props) => props.theme.bg};
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.submit {
|
||||
padding: 6px 16px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
border: none;
|
||||
background: ${(props) => props.theme.brand};
|
||||
color: ${(props) => props.theme.bg};
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.reset {
|
||||
background: transparent;
|
||||
padding: 6px 16px;
|
||||
color: ${(props) => props.theme.brand};
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.discard {
|
||||
padding: 6px 16px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
import React, { useRef, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useRef, useMemo, useEffect } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { get } from 'lodash';
|
||||
import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
|
||||
import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import MultiLineEditor from 'components/MultiLineEditor/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { uuid } from 'utils/common';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { setEnvironmentsDraft, clearEnvironmentsDraft } from 'providers/ReduxStore/slices/collections';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { getGlobalEnvironmentVariables, flattenItems, isItemARequest } from 'utils/collections';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { sensitiveFields } from './constants';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
|
||||
const EnvironmentVariables = ({ environment, setIsModified, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const addButtonRef = useRef(null);
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
|
||||
let _collection = cloneDeep(collection);
|
||||
const environmentsDraft = collection?.environmentsDraft;
|
||||
const hasDraftForThisEnv = environmentsDraft?.environmentUid === environment.uid;
|
||||
|
||||
// Track environment changes for draft restoration
|
||||
const prevEnvUidRef = React.useRef(null);
|
||||
const mountedRef = React.useRef(false);
|
||||
|
||||
let _collection = collection ? cloneDeep(collection) : {};
|
||||
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
if (_collection) {
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
}
|
||||
|
||||
// Check for non-secret variables used in sensitive fields
|
||||
const nonSecretSensitiveVarUsageMap = useMemo(() => {
|
||||
const result = {};
|
||||
if (!collection || !environment?.variables) {
|
||||
@@ -44,7 +53,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
const value = get(obj, fieldPath);
|
||||
if (typeof value === 'string') {
|
||||
varNames.forEach((varName) => {
|
||||
if (new RegExp(`\{\{\s*${varName}\s*\}\}`).test(value)) {
|
||||
if (new RegExp(`\\{\\{\\s*${varName}\\s*\\}\\}`).test(value)) {
|
||||
result[varName] = true;
|
||||
}
|
||||
});
|
||||
@@ -73,51 +82,147 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
return result;
|
||||
}, [collection, environment]);
|
||||
|
||||
const hasSensitiveUsage = (name) => !!nonSecretSensitiveVarUsageMap[name];
|
||||
|
||||
// Initial values based only on saved environment variables (not draft)
|
||||
// Draft restoration happens in a separate effect to avoid infinite loops
|
||||
const initialValues = React.useMemo(() => {
|
||||
const vars = environment.variables || [];
|
||||
return [
|
||||
...vars,
|
||||
{
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
}, [environment.uid, environment.variables]);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: environment.variables || [],
|
||||
initialValues: initialValues,
|
||||
validationSchema: Yup.array().of(
|
||||
Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
name: Yup.string()
|
||||
.required('Name cannot be empty')
|
||||
.matches(
|
||||
variableNameRegex,
|
||||
'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.'
|
||||
)
|
||||
.trim(),
|
||||
.when('$isLastRow', {
|
||||
is: true,
|
||||
then: (schema) => schema.optional(),
|
||||
otherwise: (schema) =>
|
||||
schema
|
||||
.required('Name cannot be empty')
|
||||
.matches(
|
||||
variableNameRegex,
|
||||
'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.'
|
||||
)
|
||||
.trim()
|
||||
}),
|
||||
secret: Yup.boolean(),
|
||||
type: Yup.string(),
|
||||
uid: Yup.string(),
|
||||
value: Yup.string().trim().nullable()
|
||||
value: Yup.mixed().nullable()
|
||||
})
|
||||
),
|
||||
onSubmit: (values) => {
|
||||
if (!formik.dirty) {
|
||||
toast.error('Nothing to save');
|
||||
return;
|
||||
}
|
||||
validate: (values) => {
|
||||
const errors = {};
|
||||
values.forEach((variable, index) => {
|
||||
const isLastRow = index === values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
|
||||
dispatch(saveEnvironment(cloneDeep(values), environment.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Changes saved successfully');
|
||||
formik.resetForm({ values });
|
||||
setIsModified(false);
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while saving the changes'));
|
||||
}
|
||||
if (isLastRow && isEmptyRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!variable.name || variable.name.trim() === '') {
|
||||
if (!errors[index]) errors[index] = {};
|
||||
errors[index].name = 'Name cannot be empty';
|
||||
} else if (!variableNameRegex.test(variable.name)) {
|
||||
if (!errors[index]) errors[index] = {};
|
||||
errors[index].name
|
||||
= 'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.';
|
||||
}
|
||||
});
|
||||
return Object.keys(errors).length > 0 ? errors : {};
|
||||
},
|
||||
onSubmit: () => {}
|
||||
});
|
||||
|
||||
const hasSensitiveUsage = (name) => !!nonSecretSensitiveVarUsageMap[name];
|
||||
// Restore draft values on mount or environment switch
|
||||
useEffect(() => {
|
||||
const isMount = !mountedRef.current;
|
||||
const envChanged = prevEnvUidRef.current !== null && prevEnvUidRef.current !== environment.uid;
|
||||
|
||||
// Effect to track modifications.
|
||||
React.useEffect(() => {
|
||||
setIsModified(formik.dirty);
|
||||
}, [formik.dirty]);
|
||||
prevEnvUidRef.current = environment.uid;
|
||||
mountedRef.current = true;
|
||||
|
||||
const ErrorMessage = ({ name }) => {
|
||||
if ((isMount || envChanged) && hasDraftForThisEnv && environmentsDraft?.variables) {
|
||||
formik.setValues([
|
||||
...environmentsDraft.variables,
|
||||
{
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
}
|
||||
]);
|
||||
}
|
||||
}, [environment.uid, hasDraftForThisEnv, environmentsDraft?.variables]);
|
||||
|
||||
const savedValuesJson = useMemo(() => {
|
||||
return JSON.stringify(environment.variables || []);
|
||||
}, [environment.variables]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentValues = formik.values.filter((variable) => variable.name && variable.name.trim() !== '');
|
||||
const currentValuesJson = JSON.stringify(currentValues);
|
||||
const hasActualChanges = currentValuesJson !== savedValuesJson;
|
||||
setIsModified(hasActualChanges);
|
||||
}, [formik.values, savedValuesJson, setIsModified]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
const currentValues = formik.values.filter((variable) => variable.name && variable.name.trim() !== '');
|
||||
const currentValuesJson = JSON.stringify(currentValues);
|
||||
const hasActualChanges = currentValuesJson !== savedValuesJson;
|
||||
|
||||
// Get existing draft for comparison
|
||||
const existingDraftVariables = hasDraftForThisEnv ? environmentsDraft?.variables : null;
|
||||
const existingDraftJson = existingDraftVariables ? JSON.stringify(existingDraftVariables) : null;
|
||||
|
||||
if (hasActualChanges) {
|
||||
// Only dispatch if draft values are actually different
|
||||
if (currentValuesJson !== existingDraftJson) {
|
||||
dispatch(setEnvironmentsDraft({
|
||||
collectionUid: collection.uid,
|
||||
environmentUid: environment.uid,
|
||||
variables: currentValues
|
||||
}));
|
||||
}
|
||||
} else if (hasDraftForThisEnv) {
|
||||
dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [formik.values, savedValuesJson, environment.uid, collection.uid, dispatch, hasDraftForThisEnv, environmentsDraft?.variables]);
|
||||
|
||||
const ErrorMessage = ({ name, index }) => {
|
||||
const meta = formik.getFieldMeta(name);
|
||||
const id = uuid();
|
||||
const id = `error-${name}-${index}`;
|
||||
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
const variable = formik.values[index];
|
||||
const isEmptyRow = !variable?.name || variable.name.trim() === '';
|
||||
|
||||
if (isLastRow && isEmptyRow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!meta.error || !meta.touched) {
|
||||
return null;
|
||||
}
|
||||
@@ -129,54 +234,161 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
);
|
||||
};
|
||||
|
||||
const addVariable = () => {
|
||||
const newVariable = {
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
};
|
||||
formik.setFieldValue(formik.values.length, newVariable, false);
|
||||
};
|
||||
const handleRemoveVar = useCallback((id) => {
|
||||
const currentValues = formik.values;
|
||||
|
||||
const onActivate = () => {
|
||||
dispatch(selectEnvironment(environment ? environment.uid : null, collection.uid))
|
||||
.then(() => {
|
||||
if (environment) {
|
||||
toast.success(`Environment changed to ${environment.name}`);
|
||||
onClose();
|
||||
} else {
|
||||
toast.success(`No Environments are active now`);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment'));
|
||||
};
|
||||
|
||||
const handleRemoveVar = (id) => {
|
||||
formik.setValues(formik.values.filter((variable) => variable.uid !== id));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (formik.dirty) {
|
||||
// Smooth scrolling to the changed parameter is temporarily disabled
|
||||
// due to UX issues when editing the first row in a long list of environment variables.
|
||||
// addButtonRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
if (!currentValues || currentValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
}, [formik.values, formik.dirty]);
|
||||
|
||||
const lastRow = currentValues[currentValues.length - 1];
|
||||
const isLastEmptyRow = lastRow?.uid === id && (!lastRow.name || lastRow.name.trim() === '');
|
||||
|
||||
if (isLastEmptyRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredValues = currentValues.filter((variable) => variable.uid !== id);
|
||||
|
||||
const hasEmptyLastRow
|
||||
= filteredValues.length > 0
|
||||
&& (!filteredValues[filteredValues.length - 1].name
|
||||
|| filteredValues[filteredValues.length - 1].name.trim() === '');
|
||||
|
||||
const newValues = hasEmptyLastRow
|
||||
? filteredValues
|
||||
: [
|
||||
...filteredValues,
|
||||
{
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
|
||||
formik.setValues(newValues);
|
||||
}, [formik.values]);
|
||||
|
||||
const handleNameChange = (index, e) => {
|
||||
formik.handleChange(e);
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
|
||||
if (isLastRow) {
|
||||
const newVariable = {
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
};
|
||||
setTimeout(() => {
|
||||
formik.setFieldValue(formik.values.length, newVariable, false);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNameBlur = (index) => {
|
||||
formik.setFieldTouched(`${index}.name`, true, true);
|
||||
};
|
||||
|
||||
const handleNameKeyDown = (index, e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
formik.setFieldTouched(`${index}.name`, true, true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const variablesToSave = formik.values.filter((variable) => variable.name && variable.name.trim() !== '');
|
||||
const savedValues = environment.variables || [];
|
||||
|
||||
const hasChanges = JSON.stringify(variablesToSave) !== JSON.stringify(savedValues);
|
||||
if (!hasChanges) {
|
||||
toast.error('No changes to save');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasValidationErrors = variablesToSave.some((variable) => {
|
||||
if (!variable.name || variable.name.trim() === '') {
|
||||
return true;
|
||||
}
|
||||
if (!variableNameRegex.test(variable.name)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasValidationErrors) {
|
||||
toast.error('Please fix validation errors before saving');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(saveEnvironment(cloneDeep(variablesToSave), environment.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Changes saved successfully');
|
||||
const newValues = [
|
||||
...variablesToSave,
|
||||
{
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
formik.resetForm({ values: newValues });
|
||||
setIsModified(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.error('An error occurred while saving the changes');
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
formik.resetForm({ originalEnvironmentVariables });
|
||||
const originalVars = environment.variables || [];
|
||||
const resetValues = [
|
||||
...originalVars,
|
||||
{
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
}
|
||||
];
|
||||
formik.resetForm({ values: resetValues });
|
||||
setIsModified(false);
|
||||
};
|
||||
|
||||
const handleSaveRef = useRef(handleSave);
|
||||
handleSaveRef.current = handleSave;
|
||||
|
||||
useEffect(() => {
|
||||
const handleSaveEvent = () => {
|
||||
handleSaveRef.current();
|
||||
};
|
||||
|
||||
window.addEventListener('environment-save', handleSaveEvent);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('environment-save', handleSaveEvent);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-6 mb-6">
|
||||
<div className="h-[50vh] overflow-y-auto w-full">
|
||||
<table className="environment-variables">
|
||||
<StyledWrapper>
|
||||
<div className="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td className="text-center">Enabled</td>
|
||||
<td className="text-center"></td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td className="text-center">Secret</td>
|
||||
@@ -184,99 +396,112 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formik.values.map((variable, index) => (
|
||||
<tr key={variable.uid} data-testid={`env-var-row-${variable.name}`}>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<MultiLineEditor
|
||||
theme={storedTheme}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
isSecret={variable.secret}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
enableBrunoVarInfo={false}
|
||||
/>
|
||||
</div>
|
||||
{!variable.secret && hasSensitiveUsage(variable.name) && (
|
||||
<SensitiveFieldWarning
|
||||
fieldName={variable.name}
|
||||
warningMessage="This variable is used in sensitive fields. Mark it as a secret for security"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{formik.values.map((variable, index) => {
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
const isLastEmptyRow = isLastRow && isEmptyRow;
|
||||
|
||||
return (
|
||||
<tr key={variable.uid} data-testid={`env-var-row-${variable.name}`}>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
placeholder={isLastEmptyRow ? 'Name' : ''}
|
||||
onChange={(e) => handleNameChange(index, e)}
|
||||
onBlur={() => handleNameBlur(index)}
|
||||
onKeyDown={(e) => handleNameKeyDown(index, e)}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} index={index} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<MultiLineEditor
|
||||
theme={storedTheme}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
isSecret={variable.secret}
|
||||
readOnly={typeof variable.value !== 'string'}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
{typeof variable.value !== 'string' && (
|
||||
<span className="ml-2 flex items-center">
|
||||
<IconInfoCircle id={`${variable.uid}-disabled-info-icon`} className="text-muted" size={16} />
|
||||
<Tooltip
|
||||
anchorId={`${variable.uid}-disabled-info-icon`}
|
||||
content="Non-string values set via scripts are read-only and can only be updated through scripts."
|
||||
place="top"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{!variable.secret && hasSensitiveUsage(variable.name) && (
|
||||
<SensitiveFieldWarning
|
||||
fieldName={variable.name}
|
||||
warningMessage="This variable is used in sensitive fields. Mark it as a secret for security"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{!isLastEmptyRow && (
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={18} />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button
|
||||
ref={addButtonRef}
|
||||
className="btn-add-param text-link pr-2 py-3 mt-2 select-none"
|
||||
onClick={addVariable}
|
||||
id="add-variable"
|
||||
data-testid="add-variable"
|
||||
>
|
||||
+ Add Variable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-2 flex items-center" onClick={formik.handleSubmit} data-testid="save-env">
|
||||
<IconDeviceFloppy size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Save
|
||||
</button>
|
||||
<button type="submit" className="ml-2 px-1 submit btn btn-sm btn-close mt-2 flex items-center" onClick={handleReset} data-testid="reset-env">
|
||||
<IconRefresh size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Reset
|
||||
</button>
|
||||
<button type="submit" className="submit btn btn-sm btn-close mt-2 flex items-center" onClick={onActivate} data-testid="activate-env">
|
||||
<IconCircleCheck size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Activate
|
||||
</button>
|
||||
<div className="button-container">
|
||||
<div className="flex items-center">
|
||||
<button type="button" className="submit" onClick={handleSave} data-testid="save-env">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" className="submit reset ml-2" onClick={handleReset} data-testid="reset-env">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentVariables;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.bg};
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px 8px 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.title {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
|
||||
&.renaming {
|
||||
.title-input {
|
||||
flex: 1;
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
outline: none;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.inline-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.inline-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&.save {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
}
|
||||
|
||||
&.cancel {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-error {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 20px;
|
||||
margin-top: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background: ${(props) => props.theme.bg};
|
||||
border: 1px solid ${(props) => props.theme.colors.text.danger};
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
|
||||
button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&:last-child:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,59 +1,183 @@
|
||||
import { IconCopy, IconDatabase, IconEdit, IconTrash } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
import CopyEnvironment from '../../CopyEnvironment';
|
||||
import DeleteEnvironment from '../../DeleteEnvironment';
|
||||
import RenameEnvironment from '../../RenameEnvironment';
|
||||
import { IconCopy, IconEdit, IconTrash, IconCheck, IconX } from '@tabler/icons';
|
||||
import { useState, useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
import CopyEnvironment from 'components/Environments/EnvironmentSettings/CopyEnvironment';
|
||||
import DeleteEnvironment from 'components/Environments/EnvironmentSettings/DeleteEnvironment';
|
||||
import EnvironmentVariables from './EnvironmentVariables';
|
||||
import ToolHint from 'components/ToolHint/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const environments = collection?.environments || [];
|
||||
|
||||
const EnvironmentDetails = ({ environment, collection, setIsModified, onClose }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
const [openCopyModal, setOpenCopyModal] = useState(false);
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [nameError, setNameError] = useState('');
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const validateEnvironmentName = (name) => {
|
||||
if (!name || name.trim() === '') {
|
||||
return 'Name is required';
|
||||
}
|
||||
|
||||
if (name.length < 1) {
|
||||
return 'Must be at least 1 character';
|
||||
}
|
||||
|
||||
if (name.length > 255) {
|
||||
return 'Must be 255 characters or less';
|
||||
}
|
||||
|
||||
if (!validateName(name)) {
|
||||
return validateNameError(name);
|
||||
}
|
||||
|
||||
const trimmedName = name.toLowerCase().trim();
|
||||
const isDuplicate = (environments || []).some(
|
||||
(env) => env?.uid !== environment.uid && env?.name?.toLowerCase().trim() === trimmedName
|
||||
);
|
||||
if (isDuplicate) {
|
||||
return 'Environment already exists';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleRenameClick = () => {
|
||||
setIsRenaming(true);
|
||||
setNewName(environment.name);
|
||||
setNameError('');
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleSaveRename = () => {
|
||||
const error = validateEnvironmentName(newName);
|
||||
if (error) {
|
||||
setNameError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(renameEnvironment(newName, environment.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment renamed!');
|
||||
setIsRenaming(false);
|
||||
setNewName('');
|
||||
setNameError('');
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('An error occurred while renaming the environment');
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancelRename = () => {
|
||||
setIsRenaming(false);
|
||||
setNewName('');
|
||||
setNameError('');
|
||||
};
|
||||
|
||||
const handleNameChange = (e) => {
|
||||
setNewName(e.target.value);
|
||||
if (nameError) {
|
||||
setNameError('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleNameBlur = () => {
|
||||
if (newName.trim() === '') {
|
||||
handleCancelRename();
|
||||
} else {
|
||||
const error = validateEnvironmentName(newName);
|
||||
if (error) {
|
||||
setNameError(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleNameKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSaveRename();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleCancelRename();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
|
||||
{openEditModal && (
|
||||
<RenameEnvironment onClose={() => setOpenEditModal(false)} environment={environment} collection={collection} />
|
||||
)}
|
||||
<StyledWrapper>
|
||||
{openDeleteModal && (
|
||||
<DeleteEnvironment
|
||||
onClose={() => setOpenDeleteModal(false)}
|
||||
environment={environment}
|
||||
collection={collection}
|
||||
/>
|
||||
<DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={environment} collection={collection} />
|
||||
)}
|
||||
{openCopyModal && (
|
||||
<CopyEnvironment onClose={() => setOpenCopyModal(false)} environment={environment} collection={collection} />
|
||||
)}
|
||||
<div className="flex">
|
||||
<div className="flex flex-grow items-center">
|
||||
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
<span className="ml-1 font-medium break-all">{environment.name}</span>
|
||||
|
||||
<div className="header">
|
||||
<div className={`title-container ${isRenaming ? 'renaming' : ''}`}>
|
||||
{isRenaming ? (
|
||||
<>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="title-input"
|
||||
value={newName}
|
||||
onChange={handleNameChange}
|
||||
onBlur={handleNameBlur}
|
||||
onKeyDown={handleNameKeyDown}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div className="inline-actions">
|
||||
<button
|
||||
className="inline-action-btn save"
|
||||
onClick={handleSaveRename}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title="Save"
|
||||
>
|
||||
<IconCheck size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
className="inline-action-btn cancel"
|
||||
onClick={handleCancelRename}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title="Cancel"
|
||||
>
|
||||
<IconX size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<h2 className="title">{environment.name}</h2>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-x-2 pl-2">
|
||||
<ToolHint text="Edit Environment" toolhintId={`edit-${environment.uid}`}>
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
|
||||
</ToolHint>
|
||||
<ToolHint text="Copy Environment" toolhintId={`copy-${environment.uid}`}>
|
||||
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
|
||||
</ToolHint>
|
||||
<ToolHint text="Delete Environment" toolhintId={`delete-${environment.uid}`}>
|
||||
<IconTrash
|
||||
className="cursor-pointer"
|
||||
size={20}
|
||||
strokeWidth={1.5}
|
||||
onClick={() => setOpenDeleteModal(true)}
|
||||
data-testid="delete-environment-button"
|
||||
/>
|
||||
</ToolHint>
|
||||
{nameError && isRenaming && <div className="title-error">{nameError}</div>}
|
||||
<div className="actions">
|
||||
<button onClick={handleRenameClick} title="Rename">
|
||||
<IconEdit size={15} strokeWidth={1.5} />
|
||||
</button>
|
||||
<button onClick={() => setOpenCopyModal(true)} title="Copy">
|
||||
<IconCopy size={15} strokeWidth={1.5} />
|
||||
</button>
|
||||
<button onClick={() => setOpenDeleteModal(true)} title="Delete">
|
||||
<IconTrash size={15} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<EnvironmentVariables environment={environment} collection={collection} setIsModified={setIsModified} onClose={onClose} />
|
||||
<div className="content">
|
||||
<EnvironmentVariables environment={environment} setIsModified={setIsModified} collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,61 +1,284 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
margin-inline: -1rem;
|
||||
margin-block: -1.5rem;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.bg};
|
||||
|
||||
.environments-sidebar {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
|
||||
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
||||
min-height: 400px;
|
||||
.environments-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
max-height: 85vh;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.confirm-switch-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
background: ${(props) => props.theme.bg};
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* Left Sidebar */
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 16px 12px 16px;
|
||||
|
||||
.title {
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-container {
|
||||
position: relative;
|
||||
padding: 0 12px 12px 12px;
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-100%);
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 6px 8px 6px 28px;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
border-radius: 5px;
|
||||
color: ${(props) => props.theme.text};
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.environments-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.environment-item {
|
||||
min-width: 150px;
|
||||
display: block;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 8px;
|
||||
margin-bottom: 1px;
|
||||
font-size: 13px;
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
border-left: solid 2px transparent;
|
||||
text-decoration: none;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-radius: 5px;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
.environment-name {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.environment-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
.activate-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.text.muted};
|
||||
border-radius: 3px;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.workspace.button.bg};
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
}
|
||||
|
||||
.activated-checkmark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .environment-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.activated .environment-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.hoverBg};
|
||||
background: ${(props) => props.theme.workspace.button.bg};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: ${(props) => props.theme.background.surface0};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
&.renaming,
|
||||
&.creating {
|
||||
cursor: default;
|
||||
padding: 4px 4px 4px 8px;
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.workspace.button.bg};
|
||||
}
|
||||
}
|
||||
|
||||
.rename-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.environment-name-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 13px;
|
||||
padding: 2px 4px;
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
.inline-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.creating {
|
||||
.environment-name-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: 13px;
|
||||
padding: 2px 4px;
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
.inline-actions {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&.save {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => rgba(props.theme.colors.text.green, 0.1)};
|
||||
}
|
||||
}
|
||||
|
||||
&.cancel {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => rgba(props.theme.colors.text.danger, 0.1)};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.bg} !important;
|
||||
border-left: solid 2px ${(props) => props.theme.collection.environment.settings.item.border};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.item.active.hoverBg} !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-create-environment,
|
||||
.btn-import-environment {
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
color: ${(props) => props.theme.textLink};
|
||||
|
||||
span:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-import-environment {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
.env-error {
|
||||
padding: 4px 12px;
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background: ${(props) => `${props.theme.colors.text.danger}15`};
|
||||
border-radius: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,21 +1,39 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import usePrevious from 'hooks/usePrevious';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment';
|
||||
import { IconDownload, IconShieldLock, IconUpload } from '@tabler/icons';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import ManageSecrets from '../ManageSecrets';
|
||||
import CreateEnvironment from 'components/Environments/EnvironmentSettings/CreateEnvironment';
|
||||
import { IconDownload, IconUpload, IconSearch, IconPlus, IconCheck, IconX } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ConfirmSwitchEnv from './ConfirmSwitchEnv';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import ConfirmSwitchEnv from 'components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/ConfirmSwitchEnv';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { addEnvironment, renameEnvironment, selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const EnvironmentList = ({
|
||||
environments,
|
||||
activeEnvironmentUid,
|
||||
selectedEnvironment,
|
||||
setSelectedEnvironment,
|
||||
isModified,
|
||||
setIsModified,
|
||||
collection,
|
||||
setShowExportModal
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified, onClose, setShowExportModal }) => {
|
||||
const { environments } = collection;
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openImportModal, setOpenImportModal] = useState(false);
|
||||
const [openManageSecretsModal, setOpenManageSecretsModal] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [isCreatingInline, setIsCreatingInline] = useState(false);
|
||||
const [renamingEnvUid, setRenamingEnvUid] = useState(null);
|
||||
const [newEnvName, setNewEnvName] = useState('');
|
||||
const [envNameError, setEnvNameError] = useState('');
|
||||
const inputRef = useRef(null);
|
||||
const renameContainerRef = useRef(null);
|
||||
const createContainerRef = useRef(null);
|
||||
|
||||
const [switchEnvConfirmClose, setSwitchEnvConfirmClose] = useState(false);
|
||||
const [originalEnvironmentVariables, setOriginalEnvironmentVariables] = useState([]);
|
||||
@@ -24,23 +42,36 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
const prevEnvUids = usePrevious(envUids);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEnvironment) {
|
||||
const _selectedEnvironment = environments?.find((env) => env?.uid === selectedEnvironment?.uid);
|
||||
const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);
|
||||
if (hasSelectedEnvironmentChanged) {
|
||||
setSelectedEnvironment(_selectedEnvironment);
|
||||
}
|
||||
setOriginalEnvironmentVariables(selectedEnvironment.variables);
|
||||
if (!environments?.length) {
|
||||
setSelectedEnvironment(null);
|
||||
setOriginalEnvironmentVariables([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
if (environment) {
|
||||
setSelectedEnvironment(environment);
|
||||
} else {
|
||||
setSelectedEnvironment(environments && environments.length ? environments[0] : null);
|
||||
if (selectedEnvironment) {
|
||||
let _selectedEnvironment = environments?.find((env) => env?.uid === selectedEnvironment?.uid);
|
||||
|
||||
if (!_selectedEnvironment) {
|
||||
_selectedEnvironment = environments?.find((env) => env?.name === selectedEnvironment?.name);
|
||||
}
|
||||
|
||||
if (!_selectedEnvironment) {
|
||||
_selectedEnvironment = environments?.find((env) => env.uid === activeEnvironmentUid) || environments?.[0];
|
||||
}
|
||||
|
||||
const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);
|
||||
if (hasSelectedEnvironmentChanged || selectedEnvironment.uid !== _selectedEnvironment?.uid) {
|
||||
setSelectedEnvironment(_selectedEnvironment);
|
||||
}
|
||||
setOriginalEnvironmentVariables(_selectedEnvironment?.variables || []);
|
||||
return;
|
||||
}
|
||||
}, [collection, environments, selectedEnvironment]);
|
||||
|
||||
const environment = environments?.find((env) => env.uid === activeEnvironmentUid) || environments?.[0];
|
||||
|
||||
setSelectedEnvironment(environment);
|
||||
setOriginalEnvironmentVariables(environment?.variables || []);
|
||||
}, [environments, activeEnvironmentUid, selectedEnvironment]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) {
|
||||
@@ -55,6 +86,36 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
}
|
||||
}, [envUids, environments, prevEnvUids]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!renamingEnvUid) return;
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (renameContainerRef.current && !renameContainerRef.current.contains(event.target)) {
|
||||
handleCancelRename();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [renamingEnvUid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCreatingInline) return;
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (createContainerRef.current && !createContainerRef.current.contains(event.target)) {
|
||||
handleCancelCreate();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isCreatingInline]);
|
||||
|
||||
const handleEnvironmentClick = (env) => {
|
||||
if (!isModified) {
|
||||
setSelectedEnvironment(env);
|
||||
@@ -63,18 +124,141 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnvironmentDoubleClick = (env) => {
|
||||
setRenamingEnvUid(env.uid);
|
||||
setNewEnvName(env.name);
|
||||
setEnvNameError('');
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleActivateEnvironment = (e, env) => {
|
||||
e.stopPropagation();
|
||||
dispatch(selectEnvironment(env.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success(`Environment "${env.name}" activated`);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Failed to activate environment');
|
||||
});
|
||||
};
|
||||
|
||||
if (!selectedEnvironment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const validateEnvironmentName = (name, excludeUid = null) => {
|
||||
if (!name || name.trim() === '') {
|
||||
return 'Name is required';
|
||||
}
|
||||
|
||||
if (!validateName(name)) {
|
||||
return validateNameError(name);
|
||||
}
|
||||
|
||||
const trimmedName = name.toLowerCase().trim();
|
||||
const isDuplicate = environments.some(
|
||||
(env) => env?.uid !== excludeUid && env?.name?.toLowerCase().trim() === trimmedName
|
||||
);
|
||||
if (isDuplicate) {
|
||||
return 'Environment already exists';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleCreateEnvClick = () => {
|
||||
if (!isModified) {
|
||||
setOpenCreateModal(true);
|
||||
setIsCreatingInline(true);
|
||||
setNewEnvName('');
|
||||
setEnvNameError('');
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 50);
|
||||
} else {
|
||||
setSwitchEnvConfirmClose(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelCreate = () => {
|
||||
setIsCreatingInline(false);
|
||||
setNewEnvName('');
|
||||
setEnvNameError('');
|
||||
};
|
||||
|
||||
const handleSaveNewEnv = () => {
|
||||
const error = validateEnvironmentName(newEnvName);
|
||||
if (error) {
|
||||
setEnvNameError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(addEnvironment(newEnvName, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment created!');
|
||||
setIsCreatingInline(false);
|
||||
setNewEnvName('');
|
||||
setEnvNameError('');
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('An error occurred while creating the environment');
|
||||
});
|
||||
};
|
||||
|
||||
const handleEnvNameChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setNewEnvName(value);
|
||||
|
||||
if (envNameError) {
|
||||
setEnvNameError('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnvNameKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (renamingEnvUid) {
|
||||
handleSaveRename();
|
||||
} else {
|
||||
handleSaveNewEnv();
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
if (renamingEnvUid) {
|
||||
handleCancelRename();
|
||||
} else {
|
||||
handleCancelCreate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveRename = () => {
|
||||
const error = validateEnvironmentName(newEnvName, renamingEnvUid);
|
||||
if (error) {
|
||||
setEnvNameError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(renameEnvironment(newEnvName, renamingEnvUid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment renamed!');
|
||||
setRenamingEnvUid(null);
|
||||
setNewEnvName('');
|
||||
setEnvNameError('');
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('An error occurred while renaming the environment');
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancelRename = () => {
|
||||
setRenamingEnvUid(null);
|
||||
setNewEnvName('');
|
||||
setEnvNameError('');
|
||||
};
|
||||
|
||||
const handleImportClick = () => {
|
||||
if (!isModified) {
|
||||
setOpenImportModal(true);
|
||||
@@ -83,8 +267,10 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecretsClick = () => {
|
||||
setOpenManageSecretsModal(true);
|
||||
const handleExportClick = () => {
|
||||
if (setShowExportModal) {
|
||||
setShowExportModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmSwitch = (saveChanges) => {
|
||||
@@ -93,59 +279,160 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
}
|
||||
};
|
||||
|
||||
const filteredEnvironments
|
||||
= environments?.filter((env) => env.name.toLowerCase().includes(searchText.toLowerCase())) || [];
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
||||
{openImportModal && <ImportEnvironmentModal type="collection" collection={collection} onClose={() => setOpenImportModal(false)} />}
|
||||
{openManageSecretsModal && <ManageSecrets onClose={() => setOpenManageSecretsModal(false)} />}
|
||||
{openImportModal && (
|
||||
<ImportEnvironmentModal type="collection" collection={collection} onClose={() => setOpenImportModal(false)} />
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
<div>
|
||||
{switchEnvConfirmClose && (
|
||||
<div className="flex items-center justify-between tab-container px-1">
|
||||
<ConfirmSwitchEnv onCancel={() => handleConfirmSwitch(false)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="environments-sidebar flex flex-col">
|
||||
{environments
|
||||
&& environments.length
|
||||
&& environments.map((env) => (
|
||||
<ToolHint key={env.uid} text={env.name} toolhintId={env.uid} place="right">
|
||||
<div
|
||||
id={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
|
||||
>
|
||||
<span className="break-all">{env.name}</span>
|
||||
</div>
|
||||
</ToolHint>
|
||||
))}
|
||||
<div className="btn-create-environment" onClick={() => handleCreateEnvClick()}>
|
||||
+ <span>Create</span>
|
||||
</div>
|
||||
<div className="environments-container">
|
||||
{switchEnvConfirmClose && (
|
||||
<div className="confirm-switch-overlay">
|
||||
<ConfirmSwitchEnv onCancel={() => handleConfirmSwitch(false)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-auto btn-import-environment">
|
||||
<div className="flex items-center" onClick={() => handleImportClick()}>
|
||||
<IconDownload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Import</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2" onClick={() => setShowExportModal(true)}>
|
||||
<IconUpload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Export</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2" onClick={() => handleSecretsClick()}>
|
||||
<IconShieldLock size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Managing Secrets</span>
|
||||
</div>
|
||||
<div className="sidebar">
|
||||
<div className="sidebar-header">
|
||||
<h2 className="title">Environments</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="btn-action" onClick={() => handleCreateEnvClick()} title="Create environment">
|
||||
<IconPlus size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
<button className="btn-action" onClick={() => handleImportClick()} title="Import environment">
|
||||
<IconDownload size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
<button className="btn-action" onClick={() => handleExportClick()} title="Export environment">
|
||||
<IconUpload size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="search-container">
|
||||
<IconSearch size={14} strokeWidth={1.5} className="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search environments..."
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="environments-list">
|
||||
{filteredEnvironments.map((env) => (
|
||||
<div
|
||||
key={env.uid}
|
||||
id={env.uid}
|
||||
className={`environment-item ${selectedEnvironment.uid === env.uid ? 'active' : ''} ${renamingEnvUid === env.uid ? 'renaming' : ''} ${activeEnvironmentUid === env.uid ? 'activated' : ''}`}
|
||||
onClick={() => renamingEnvUid !== env.uid && handleEnvironmentClick(env)}
|
||||
onDoubleClick={() => handleEnvironmentDoubleClick(env)}
|
||||
>
|
||||
{renamingEnvUid === env.uid ? (
|
||||
<div className="rename-container" ref={renameContainerRef}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="environment-name-input"
|
||||
value={newEnvName}
|
||||
onChange={handleEnvNameChange}
|
||||
onKeyDown={handleEnvNameKeyDown}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div className="inline-actions">
|
||||
<button
|
||||
className="inline-action-btn save"
|
||||
onClick={handleSaveRename}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title="Save"
|
||||
>
|
||||
<IconCheck size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
className="inline-action-btn cancel"
|
||||
onClick={handleCancelRename}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title="Cancel"
|
||||
>
|
||||
<IconX size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="environment-name">{env.name}</span>
|
||||
<div className="environment-actions">
|
||||
{activeEnvironmentUid === env.uid ? (
|
||||
<div className="activated-checkmark" title="Active environment">
|
||||
<IconCheck size={16} strokeWidth={2} />
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="activate-btn"
|
||||
onClick={(e) => handleActivateEnvironment(e, env)}
|
||||
title="Activate environment"
|
||||
>
|
||||
<IconCheck size={16} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isCreatingInline && (
|
||||
<div className="environment-item creating" ref={createContainerRef}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="environment-name-input"
|
||||
value={newEnvName}
|
||||
onChange={handleEnvNameChange}
|
||||
onKeyDown={handleEnvNameKeyDown}
|
||||
placeholder="Environment name..."
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div className="inline-actions">
|
||||
<button
|
||||
className="inline-action-btn save"
|
||||
onClick={handleSaveNewEnv}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title="Save"
|
||||
>
|
||||
<IconCheck size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
className="inline-action-btn cancel"
|
||||
onClick={handleCancelCreate}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title="Cancel"
|
||||
>
|
||||
<IconX size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{envNameError && (isCreatingInline || renamingEnvUid) && <div className="env-error">{envNameError}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EnvironmentDetails
|
||||
environment={selectedEnvironment}
|
||||
collection={collection}
|
||||
setIsModified={setIsModified}
|
||||
originalEnvironmentVariables={originalEnvironmentVariables}
|
||||
onClose={onClose}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ManageSecrets = ({ onClose }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title="Manage Secrets" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
<p>In any collection, there are secrets that need to be managed.</p>
|
||||
<p className="mt-2">These secrets can be anything such as API keys, passwords, or tokens.</p>
|
||||
<p className="mt-4">Bruno offers three approaches to manage secrets in collections.</p>
|
||||
<p className="mt-2">
|
||||
Read more about it in our{' '}
|
||||
<a
|
||||
href="https://docs.usebruno.com/secrets-management/overview"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-link hover:underline"
|
||||
>
|
||||
docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageSecrets;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user