mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-09 06:55:03 +00:00
Merge remote-tracking branch 'origin/main' into oauth2_additional_params
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const AwsV4Auth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = get(collection, 'root.request.auth.awsv4', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -131,7 +135,7 @@ const AwsV4Auth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<div className="single-line-editor-wrapper mb-2 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
@@ -140,6 +144,7 @@ const AwsV4Auth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="awsv4-secret-access-key" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Session Token</label>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const BasicAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = get(collection, 'root.request.auth.basic', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -55,7 +59,7 @@ const BasicAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -64,6 +68,7 @@ const BasicAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const BearerAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const bearerToken = get(collection, 'root.request.auth.bearer.token', '');
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(bearerToken);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -30,7 +34,7 @@ const BearerAuth = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Token</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
theme={storedTheme}
|
||||
@@ -39,6 +43,7 @@ const BearerAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="bearer-token" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const DigestAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const digestAuth = get(collection, 'root.request.auth.digest', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -55,7 +59,7 @@ const DigestAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -64,6 +68,7 @@ const DigestAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="digest-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -18,6 +20,8 @@ const NTLMAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = get(collection, 'root.request.auth.ntlm', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -82,7 +86,7 @@ const NTLMAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -91,6 +95,7 @@ const NTLMAuth = ({ collection }) => {
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="ntlm-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Domain</label>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const WsseAuth = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const wsseAuth = get(collection, 'root.request.auth.wsse', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
@@ -55,14 +59,16 @@ const WsseAuth = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ const Docs = ({ collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-1 h-full w-full relative flex flex-col">
|
||||
<StyledWrapper className="h-full w-full relative flex flex-col">
|
||||
<div className='flex flex-row w-full justify-between items-center mb-4'>
|
||||
<div className='text-lg font-medium flex items-center gap-2'>
|
||||
<IconFileText size={20} strokeWidth={1.5} />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
@@ -7,19 +7,30 @@ import { useTheme } from 'providers/Theme';
|
||||
import {
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader
|
||||
deleteCollectionHeader,
|
||||
setCollectionHeaders
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
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';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const 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(
|
||||
@@ -63,6 +74,22 @@ const Headers = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
if (isBulkEditMode) {
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Add request headers that will be sent with every request in this collection.
|
||||
</div>
|
||||
<BulkEditor
|
||||
params={headers}
|
||||
onChange={handleBulkHeadersChange}
|
||||
onToggle={toggleBulkEditMode}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
@@ -141,9 +168,14 @@ const Headers = ({ collection }) => {
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<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>
|
||||
<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}>
|
||||
|
||||
@@ -132,7 +132,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative px-4 py-4 overflow-scroll">
|
||||
<StyledWrapper className="flex flex-col h-full relative px-4 py-4 overflow-hidden">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('overview')} role="tab" onClick={() => setTab('overview')}>
|
||||
Overview
|
||||
@@ -169,7 +169,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
{clientCertConfig.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
</div>
|
||||
<section className="mt-4 h-full overflow-scroll">{getTabPanel(tab)}</section>
|
||||
<section className="mt-4 h-full overflow-auto">{getTabPanel(tab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const sensitiveFields = [
|
||||
'request.auth.oauth2.clientSecret',
|
||||
'request.auth.basic.password',
|
||||
'request.auth.digest.password',
|
||||
'request.auth.wsse.password',
|
||||
'request.auth.ntlm.password',
|
||||
'request.auth.awsv4.secretAccessKey',
|
||||
'request.auth.bearer.token'
|
||||
];
|
||||
|
||||
export { sensitiveFields };
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import React, { useRef, useEffect, useMemo } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { get } from 'lodash';
|
||||
import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -13,7 +14,9 @@ import { variableNameRegex } from 'utils/common/regex';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { getGlobalEnvironmentVariables, flattenItems, isItemARequest } from 'utils/collections';
|
||||
import { sensitiveFields } from './constants';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -26,6 +29,50 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
|
||||
const nonSecretSensitiveVarUsageMap = useMemo(() => {
|
||||
const result = {};
|
||||
if (!collection || !environment?.variables) {
|
||||
return result;
|
||||
}
|
||||
const nonSecretVars = environment.variables.filter((v) => v.enabled && !v.secret && v.name);
|
||||
if (!nonSecretVars.length) {
|
||||
return result;
|
||||
}
|
||||
const varNames = new Set(nonSecretVars.map((v) => v.name));
|
||||
|
||||
const checkSensitiveField = (obj, fieldPath) => {
|
||||
const value = get(obj, fieldPath);
|
||||
if (typeof value === 'string') {
|
||||
varNames.forEach((varName) => {
|
||||
if (new RegExp(`\{\{\s*${varName}\s*\}\}`).test(value)) {
|
||||
result[varName] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getObjectToProcess = (item) => {
|
||||
if (isItemARequest(item)) {
|
||||
return item.draft || item;
|
||||
}
|
||||
return item.root;
|
||||
};
|
||||
|
||||
const collectionObj = getObjectToProcess(collection);
|
||||
sensitiveFields.forEach((fieldPath) => {
|
||||
checkSensitiveField(collectionObj, fieldPath);
|
||||
});
|
||||
|
||||
const items = flattenItems(collection.items || []);
|
||||
items.forEach((item) => {
|
||||
const objToProcess = getObjectToProcess(item);
|
||||
sensitiveFields.forEach((fieldPath) => {
|
||||
checkSensitiveField(objToProcess, fieldPath);
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}, [collection, environment]);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: environment.variables || [],
|
||||
@@ -61,6 +108,8 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
}
|
||||
});
|
||||
|
||||
const hasSensitiveUsage = (name) => !!nonSecretSensitiveVarUsageMap[name];
|
||||
|
||||
// Effect to track modifications.
|
||||
React.useEffect(() => {
|
||||
setIsModified(formik.dirty);
|
||||
@@ -163,7 +212,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap">
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
@@ -174,6 +223,12 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
/>
|
||||
</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
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import React from 'react';
|
||||
import React, { useState } 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 { addFolderHeader, updateFolderHeader, deleteFolderHeader } from 'providers/ReduxStore/slices/collections';
|
||||
import { addFolderHeader, updateFolderHeader, deleteFolderHeader, setFolderHeaders } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
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';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = get(folder, 'root.request.headers', []);
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
setIsBulkEditMode(!isBulkEditMode);
|
||||
};
|
||||
|
||||
const handleBulkHeadersChange = (newHeaders) => {
|
||||
dispatch(setFolderHeaders({ collectionUid: collection.uid, folderUid: folder.uid, headers: newHeaders }));
|
||||
};
|
||||
|
||||
const addHeader = () => {
|
||||
dispatch(
|
||||
@@ -62,6 +72,22 @@ const Headers = ({ collection, folder }) => {
|
||||
);
|
||||
};
|
||||
|
||||
if (isBulkEditMode) {
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Request headers that will be sent with every request inside this folder.
|
||||
</div>
|
||||
<BulkEditor
|
||||
params={headers}
|
||||
onChange={handleBulkHeadersChange}
|
||||
onToggle={toggleBulkEditMode}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
@@ -141,9 +167,14 @@ const Headers = ({ collection, folder }) => {
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<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>
|
||||
<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}>
|
||||
|
||||
@@ -74,7 +74,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full overflow-scroll">
|
||||
<StyledWrapper className="flex flex-col h-full overflow-auto">
|
||||
<div className="flex flex-col h-full relative px-4 py-4">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
@@ -101,7 +101,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
Docs
|
||||
</div>
|
||||
</div>
|
||||
<section className={`flex mt-4 h-full overflow-scroll`}>{getTabPanel(tab)}</section>
|
||||
<section className={`flex mt-4 h-full overflow-auto`}>{getTabPanel(tab)}</section>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -6,13 +6,16 @@ import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { update } from 'lodash';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
|
||||
const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = get(request, 'auth.awsv4', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -144,7 +147,7 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<div className="single-line-editor-wrapper mb-2 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
@@ -155,6 +158,8 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
|
||||
{showWarning && <SensitiveFieldWarning fieldName="awsv4-secret-access-key" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Session Token</label>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = get(request, 'auth.basic', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -63,7 +67,7 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -74,6 +78,7 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -13,6 +15,8 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
|
||||
// Use the request prop directly like OAuth2ClientCredentials does
|
||||
const bearerToken = get(request, 'auth.bearer.token', '');
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(bearerToken);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -36,7 +40,7 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Token</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
theme={storedTheme}
|
||||
@@ -47,6 +51,7 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="bearer-token" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -11,6 +13,8 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const digestAuth = get(request, 'auth.digest', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -62,7 +66,7 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -73,6 +77,7 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="digest-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = get(request, 'auth.ntlm', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -80,7 +84,7 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -91,6 +95,7 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="ntlm-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Domain</label>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -10,13 +11,14 @@ import { inputsConfig } from './inputsConfig';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
import AdditionalParams from '../AdditionalParams/index';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const {
|
||||
callbackUrl,
|
||||
@@ -133,12 +135,15 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
const value = oAuth[key] || '';
|
||||
const { showWarning, warningMessage } = isSensitive(value);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<div className="single-line-editor-wrapper flex-1 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
value={value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -147,6 +152,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
{isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -10,13 +11,14 @@ import Dropdown from 'components/Dropdown';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
import AdditionalParams from '../AdditionalParams/index';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
|
||||
const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
|
||||
const {
|
||||
@@ -99,12 +101,15 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
const value = oAuth[key] || '';
|
||||
const { showWarning, warningMessage } = isSensitive(value);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<div className="single-line-editor-wrapper flex-1 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
value={value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -113,6 +118,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
{isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -10,14 +11,15 @@ import Dropdown from 'components/Dropdown';
|
||||
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
|
||||
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
|
||||
import AdditionalParams from '../AdditionalParams/index';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index';
|
||||
|
||||
const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = get(request, 'auth.oauth2', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
|
||||
const {
|
||||
accessTokenUrl,
|
||||
@@ -102,12 +104,15 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
</div>
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
const value = oAuth[key] || '';
|
||||
const { showWarning, warningMessage } = isSensitive(value);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4 w-full" key={`input-${key}`}>
|
||||
<label className="block min-w-[140px]">{label}</label>
|
||||
<div className="single-line-editor-wrapper flex-1">
|
||||
<div className="single-line-editor-wrapper flex-1 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
value={value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
@@ -116,6 +121,7 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
|
||||
item={item}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
{isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -12,6 +14,8 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const wsseAuth = get(request, 'auth.wsse', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
|
||||
@@ -63,7 +67,7 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -74,6 +78,7 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
item={item}
|
||||
isSecret={true}
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" message={warningMessage} />}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -109,7 +109,7 @@ const Auth = ({ item, collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-1 overflow-y-scroll">
|
||||
<StyledWrapper className="w-full mt-1 overflow-auto">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<AuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
|
||||
@@ -176,7 +176,7 @@ const ResponsePane = ({ item, collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={`flex flex-col min-h-0 relative px-4 auto overflow-scroll`}
|
||||
className={`flex flex-col min-h-0 relative px-4 auto overflow-auto`}
|
||||
style={{
|
||||
flex: '1 1 0',
|
||||
height: hasScriptError && showScriptErrorCard ? 'auto' : '100%'
|
||||
|
||||
@@ -96,7 +96,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative overflow-scroll">
|
||||
<StyledWrapper className="flex flex-col h-full relative overflow-auto">
|
||||
<div className="flex items-center tabs overflow-visible" role="tablist">
|
||||
<div className={getTabClassname('response')} role="tab" onClick={() => selectTab('response')}>
|
||||
Response
|
||||
@@ -128,7 +128,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
<ResponseSize size={size} />
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex flex-col flex-grow overflow-scroll">
|
||||
<section className="flex flex-col flex-grow overflow-auto">
|
||||
{hasScriptError && showScriptErrorCard && (
|
||||
<ScriptError
|
||||
item={item}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
background-color: ${props => props.theme.sidebar.bg};
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid ${props => props.theme.sidebar.dragbar};
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.counter {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-select-all,
|
||||
.btn-reset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: ${props => props.theme.textLink};
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.request-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: ${props => props.theme.console.scrollbarThumb};
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.loading-message,
|
||||
.empty-message {
|
||||
padding: 0.75rem;
|
||||
color: ${props => props.theme.colors.text.muted};
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.requests-container {
|
||||
padding: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.request-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.25rem;
|
||||
position: relative;
|
||||
height: 2.5rem;
|
||||
border: 1px solid transparent;
|
||||
background-color: ${props => props.theme.sidebar.bg};
|
||||
transition: transform 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&.is-selected {
|
||||
background-color: ${props => props.theme.requestTabs.active.bg};
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
opacity: 0.5;
|
||||
background-color: ${props => props.theme.sidebar.bg};
|
||||
border: 1px dashed ${props => props.theme.sidebar.dragbar};
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: ${props => props.theme.dragAndDrop?.border || props.theme.textLink};
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
&.drop-target-above {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
height: 2px;
|
||||
background: ${props => props.theme.dragAndDrop?.border || props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
&.drop-target-below {
|
||||
&::after {
|
||||
opacity: 1;
|
||||
height: 2px;
|
||||
background: ${props => props.theme.dragAndDrop?.border || props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
margin-right: 0.25rem;
|
||||
color: ${props => props.theme.sidebar.muted};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: ${props => props.theme.text};
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
color: ${props => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
|
||||
.checkbox {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 1px solid ${props => props.theme.sidebar.dragbar};
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.1s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: ${props => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.method {
|
||||
font-family: monospace;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-right: 0.5rem;
|
||||
min-width: 3rem;
|
||||
color: ${props => props.theme.sidebar.muted}; // Default color for unknown methods
|
||||
|
||||
&.method-get {
|
||||
color: ${props => props.theme.request.methods.get};
|
||||
}
|
||||
|
||||
&.method-post {
|
||||
color: ${props => props.theme.request.methods.post};
|
||||
}
|
||||
|
||||
&.method-put {
|
||||
color: ${props => props.theme.request.methods.put};
|
||||
}
|
||||
|
||||
&.method-delete {
|
||||
color: ${props => props.theme.request.methods.delete};
|
||||
}
|
||||
|
||||
&.method-patch {
|
||||
color: ${props => props.theme.request.methods.patch};
|
||||
}
|
||||
|
||||
&.method-options {
|
||||
color: ${props => props.theme.request.methods.options};
|
||||
}
|
||||
|
||||
&.method-head {
|
||||
color: ${props => props.theme.request.methods.head};
|
||||
}
|
||||
}
|
||||
|
||||
.request-name {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.folder-path {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: ${props => props.theme.sidebar.muted};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,327 @@
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
import { IconGripVertical, IconCheck, IconAdjustmentsAlt } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { isItemARequest } from 'utils/collections';
|
||||
import path from 'utils/common/path';
|
||||
import { cloneDeep, get } from 'lodash';
|
||||
|
||||
const ItemTypes = {
|
||||
REQUEST_ITEM: 'request-item'
|
||||
};
|
||||
|
||||
const RequestItem = ({ item, index, moveItem, isSelected, onSelect, onDrop }) => {
|
||||
const ref = useRef(null);
|
||||
const [dropType, setDropType] = useState(null);
|
||||
|
||||
const determineDropType = (monitor) => {
|
||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
if (!hoverBoundingRect || !clientOffset) return null;
|
||||
|
||||
const clientY = clientOffset.y - hoverBoundingRect.top;
|
||||
const middleY = hoverBoundingRect.height / 2;
|
||||
|
||||
return clientY < middleY ? 'above' : 'below';
|
||||
};
|
||||
|
||||
const [{ isDragging }, drag, preview] = useDrag({
|
||||
type: ItemTypes.REQUEST_ITEM,
|
||||
item: { uid: item.uid, name: item.name, request: item.request, index },
|
||||
collect: (monitor) => ({ isDragging: monitor.isDragging() }),
|
||||
options: {
|
||||
dropEffect: "move"
|
||||
},
|
||||
end: (draggedItem, monitor) => {
|
||||
if (monitor.didDrop()) {
|
||||
onDrop();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [{ isOver, canDrop }, drop] = useDrop({
|
||||
accept: ItemTypes.REQUEST_ITEM,
|
||||
hover: (draggedItem, monitor) => {
|
||||
if (draggedItem.uid === item.uid) {
|
||||
setDropType(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const dropType = determineDropType(monitor);
|
||||
setDropType(dropType);
|
||||
},
|
||||
drop: (draggedItem, monitor) => {
|
||||
if (draggedItem.uid === item.uid) return;
|
||||
|
||||
const dropType = determineDropType(monitor);
|
||||
let targetIndex = index;
|
||||
|
||||
if (dropType === 'below') {
|
||||
targetIndex = index + 1;
|
||||
}
|
||||
|
||||
if (draggedItem.index < targetIndex) {
|
||||
targetIndex = targetIndex - 1;
|
||||
}
|
||||
|
||||
moveItem(draggedItem.uid, targetIndex);
|
||||
setDropType(null);
|
||||
return { item: draggedItem };
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
canDrop: monitor.canDrop()
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
preview(getEmptyImage(), { captureDraggingState: true });
|
||||
}, []);
|
||||
|
||||
// Clear drop type when not hovering
|
||||
useEffect(() => {
|
||||
if (!isOver) {
|
||||
setDropType(null);
|
||||
}
|
||||
}, [isOver]);
|
||||
|
||||
drag(drop(ref));
|
||||
|
||||
const itemClasses = [
|
||||
'request-item',
|
||||
isDragging ? 'is-dragging' : '',
|
||||
isSelected ? 'is-selected' : '',
|
||||
isOver && canDrop && dropType === 'above' ? 'drop-target-above' : '',
|
||||
isOver && canDrop && dropType === 'below' ? 'drop-target-below' : ''
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
return (
|
||||
<div ref={ref} className={itemClasses}>
|
||||
<div className="drag-handle">
|
||||
<IconGripVertical size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<div className="checkbox-container" onClick={() => onSelect(item)}>
|
||||
<div className="checkbox">
|
||||
{isSelected && <IconCheck size={12} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`method method-${item.request?.method.toLowerCase()}`}>
|
||||
{item.request?.method.toUpperCase()}
|
||||
</div>
|
||||
|
||||
<div className="request-name">
|
||||
<span>{item.name}</span>
|
||||
{item.folderPath && (
|
||||
<span className="folder-path">{item.folderPath}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [flattenedRequests, setFlattenedRequests] = useState([]);
|
||||
const [originalRequests, setOriginalRequests] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const flattenRequests = useCallback((collection) => {
|
||||
const result = [];
|
||||
|
||||
const processItems = (items) => {
|
||||
if (!items?.length) return;
|
||||
|
||||
items.forEach(item => {
|
||||
if (isItemARequest(item) && !item.partial) {
|
||||
const relativePath = path.relative(collection.pathname, path.dirname(item.pathname));
|
||||
const folderPath = relativePath !== '.' ? relativePath : '';
|
||||
|
||||
result.push({
|
||||
...item,
|
||||
folderPath: folderPath.replace(/\\/g, '/')
|
||||
});
|
||||
}
|
||||
|
||||
if (item.items?.length) {
|
||||
processItems(item.items);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
processItems(collection.items);
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const structureCopy = cloneDeep(collection);
|
||||
const requests = flattenRequests(structureCopy);
|
||||
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
if (savedConfiguration?.requestItemsOrder?.length > 0) {
|
||||
const orderedRequests = [];
|
||||
const requestMap = new Map(requests.map(req => [req.uid, req]));
|
||||
|
||||
savedConfiguration.requestItemsOrder.forEach(uid => {
|
||||
const request = requestMap.get(uid);
|
||||
if (request) {
|
||||
orderedRequests.push(request);
|
||||
requestMap.delete(uid);
|
||||
}
|
||||
});
|
||||
|
||||
requestMap.forEach(request => {
|
||||
orderedRequests.push(request);
|
||||
});
|
||||
|
||||
setFlattenedRequests(orderedRequests);
|
||||
} else {
|
||||
setFlattenedRequests(requests);
|
||||
}
|
||||
|
||||
setOriginalRequests(cloneDeep(requests));
|
||||
} catch (error) {
|
||||
console.error("Error loading collection structure:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [collection, flattenRequests]);
|
||||
|
||||
const moveItem = useCallback((draggedItemUid, hoverIndex) => {
|
||||
setFlattenedRequests((prevRequests) => {
|
||||
const dragIndex = prevRequests.findIndex(item => item.uid === draggedItemUid);
|
||||
|
||||
if (dragIndex === -1 || dragIndex === hoverIndex) {
|
||||
return prevRequests;
|
||||
}
|
||||
|
||||
const updatedRequests = [...prevRequests];
|
||||
const [draggedItem] = updatedRequests.splice(dragIndex, 1);
|
||||
updatedRequests.splice(hoverIndex, 0, draggedItem);
|
||||
|
||||
return updatedRequests;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback(() => {
|
||||
const selectedUids = new Set(selectedItems);
|
||||
|
||||
setFlattenedRequests(currentRequests => {
|
||||
const newOrderedSelectedUids = currentRequests
|
||||
.filter(item => selectedUids.has(item.uid))
|
||||
.map(item => item.uid);
|
||||
|
||||
const allRequestUidsOrder = currentRequests.map(item => item.uid);
|
||||
|
||||
setSelectedItems(newOrderedSelectedUids);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, newOrderedSelectedUids, allRequestUidsOrder));
|
||||
|
||||
return currentRequests;
|
||||
});
|
||||
}, [selectedItems, collection.uid, dispatch, setSelectedItems]);
|
||||
|
||||
const handleRequestSelect = useCallback((item) => {
|
||||
try {
|
||||
if (selectedItems.includes(item.uid)) {
|
||||
const newSelectedUids = selectedItems.filter(uid => uid !== item.uid);
|
||||
setSelectedItems(newSelectedUids);
|
||||
|
||||
const allRequestUidsOrder = flattenedRequests.map(item => item.uid);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, newSelectedUids, allRequestUidsOrder));
|
||||
} else {
|
||||
const newSelectedUids = [...selectedItems, item.uid];
|
||||
|
||||
const orderedSelectedUids = flattenedRequests
|
||||
.filter(req => newSelectedUids.includes(req.uid))
|
||||
.map(req => req.uid);
|
||||
|
||||
setSelectedItems(orderedSelectedUids);
|
||||
|
||||
const allRequestUidsOrder = flattenedRequests.map(item => item.uid);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, orderedSelectedUids, allRequestUidsOrder));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error selecting item:", error);
|
||||
}
|
||||
}, [selectedItems, setSelectedItems, flattenedRequests, dispatch, collection.uid]);
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
try {
|
||||
const allRequestUidsOrder = flattenedRequests.map(item => item.uid);
|
||||
|
||||
if (selectedItems.length === flattenedRequests.length) {
|
||||
setSelectedItems([]);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, [], allRequestUidsOrder));
|
||||
} else {
|
||||
setSelectedItems(allRequestUidsOrder);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, allRequestUidsOrder, allRequestUidsOrder));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error selecting/deselecting all items:", error);
|
||||
}
|
||||
}, [flattenedRequests, selectedItems, setSelectedItems, dispatch, collection.uid]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
try {
|
||||
setFlattenedRequests(cloneDeep(originalRequests));
|
||||
setSelectedItems([]);
|
||||
dispatch(updateRunnerConfiguration(collection.uid, [], []));
|
||||
} catch (error) {
|
||||
console.error("Error resetting configuration:", error);
|
||||
}
|
||||
}, [originalRequests, setSelectedItems, collection.uid, dispatch]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="header">
|
||||
<div className="counter">
|
||||
{selectedItems.length} of {flattenedRequests.length} selected
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button className="btn-select-all" onClick={handleSelectAll}>
|
||||
{selectedItems.length === flattenedRequests.length ? "Deselect All" : "Select All"}
|
||||
</button>
|
||||
<button className="btn-reset" onClick={handleReset} title="Reset selection and order">
|
||||
<IconAdjustmentsAlt size={16} strokeWidth={1.5} />
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="request-list">
|
||||
{isLoading ? (
|
||||
<div className="loading-message">Loading requests...</div>
|
||||
) : flattenedRequests.length === 0 ? (
|
||||
<div className="empty-message">No requests found in this collection</div>
|
||||
) : (
|
||||
<div className="requests-container">
|
||||
{flattenedRequests.map((item, idx) => {
|
||||
const isSelected = selectedItems.includes(item.uid);
|
||||
|
||||
return (
|
||||
<RequestItem
|
||||
key={item.uid}
|
||||
item={item}
|
||||
index={idx}
|
||||
isSelected={isSelected}
|
||||
onSelect={() => handleRequestSelect(item)}
|
||||
moveItem={moveItem}
|
||||
onDrop={handleDrop}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RunConfigurationPanel;
|
||||
@@ -89,13 +89,15 @@ const RunnerTags = ({ collectionUid, className = '' }) => {
|
||||
return (
|
||||
<div className={`mt-6 flex flex-col ${className}`}>
|
||||
<div className="flex gap-2">
|
||||
<label className="block font-medium">Filter requests with tags</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
id="filter-tags"
|
||||
type="radio"
|
||||
name="filterMode"
|
||||
checked={tagsEnabled}
|
||||
onChange={() => setTagsEnabled(!tagsEnabled)}
|
||||
/>
|
||||
<label htmlFor="filter-tags" className="block font-medium">Filter requests with tags</label>
|
||||
</div>
|
||||
{tagsEnabled && (
|
||||
<div className="flex flex-row mt-4 gap-4 w-full">
|
||||
|
||||
@@ -2,15 +2,17 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import path from 'utils/common/path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder, cancelRunnerExecution } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { runCollectionFolder, cancelRunnerExecution, mountCollection, updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun } from '@tabler/icons';
|
||||
import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconLoader2 } from '@tabler/icons';
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
import RunnerTags from './RunnerTags/index';
|
||||
import RunConfigurationPanel from './RunConfigurationPanel';
|
||||
import { getRequestItemsForCollectionRun } from 'utils/collections/index';
|
||||
import { updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const getDisplayName = (fullPath, pathname, name = '') => {
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
@@ -25,25 +27,27 @@ const getTestStatus = (results) => {
|
||||
};
|
||||
|
||||
const allTestsPassed = (item) => {
|
||||
return item.status !== 'error' &&
|
||||
item.testStatus === 'pass' &&
|
||||
item.assertionStatus === 'pass' &&
|
||||
item.preRequestTestStatus === 'pass' &&
|
||||
item.postResponseTestStatus === 'pass';
|
||||
return item.status !== 'error' &&
|
||||
item.testStatus === 'pass' &&
|
||||
item.assertionStatus === 'pass' &&
|
||||
item.preRequestTestStatus === 'pass' &&
|
||||
item.postResponseTestStatus === 'pass';
|
||||
};
|
||||
|
||||
const anyTestFailed = (item) => {
|
||||
return item.status === 'error' ||
|
||||
item.testStatus === 'fail' ||
|
||||
item.assertionStatus === 'fail' ||
|
||||
item.preRequestTestStatus === 'fail' ||
|
||||
item.postResponseTestStatus === 'fail';
|
||||
return item.status === 'error' ||
|
||||
item.testStatus === 'fail' ||
|
||||
item.assertionStatus === 'fail' ||
|
||||
item.preRequestTestStatus === 'fail' ||
|
||||
item.postResponseTestStatus === 'fail';
|
||||
};
|
||||
|
||||
export default function RunnerResults({ collection }) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [delay, setDelay] = useState(null);
|
||||
const [selectedRequestItems, setSelectedRequestItems] = useState([]);
|
||||
const [configureMode, setConfigureMode] = useState(false);
|
||||
|
||||
// ref for the runner output body
|
||||
const runnerBodyRef = useRef();
|
||||
@@ -62,6 +66,22 @@ export default function RunnerResults({ collection }) {
|
||||
autoScrollRunnerBody();
|
||||
}, [collection, setSelectedItem]);
|
||||
|
||||
useEffect(() => {
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
if (runnerInfo.status === 'running') {
|
||||
setConfigureMode(false);
|
||||
}
|
||||
}, [collection.runnerResult]);
|
||||
|
||||
useEffect(() => {
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
if (savedConfiguration && configureMode) {
|
||||
if (savedConfiguration.selectedRequestItems) {
|
||||
setSelectedRequestItems(savedConfiguration.selectedRequestItems);
|
||||
}
|
||||
}
|
||||
}, [collection.runnerConfiguration, configureMode]);
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
|
||||
@@ -103,18 +123,39 @@ export default function RunnerResults({ collection }) {
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const ensureCollectionIsMounted = () => {
|
||||
if(collection.mountStatus === 'mounted'){
|
||||
return;
|
||||
}
|
||||
dispatch(mountCollection({
|
||||
collectionUid: collection.uid,
|
||||
collectionPathname: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig
|
||||
}));
|
||||
};
|
||||
|
||||
const runCollection = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags));
|
||||
if (configureMode && selectedRequestItems.length > 0) {
|
||||
dispatch(updateRunnerConfiguration(collection.uid, selectedRequestItems, selectedRequestItems));
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags, selectedRequestItems));
|
||||
} else {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags));
|
||||
}
|
||||
};
|
||||
|
||||
const runAgain = () => {
|
||||
ensureCollectionIsMounted();
|
||||
// Get the saved configuration to determine what to run
|
||||
const savedConfiguration = get(collection, 'runnerConfiguration', null);
|
||||
const savedSelectedItems = savedConfiguration?.selectedRequestItems || [];
|
||||
dispatch(
|
||||
runCollectionFolder(
|
||||
collection.uid,
|
||||
runnerInfo.folderUid,
|
||||
runnerInfo.isRecursive,
|
||||
true,
|
||||
Number(delay),
|
||||
tagsEnabled && tags
|
||||
tagsEnabled && tags,
|
||||
savedSelectedItems
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -125,12 +166,25 @@ export default function RunnerResults({ collection }) {
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
setSelectedRequestItems([]);
|
||||
setConfigureMode(false);
|
||||
};
|
||||
|
||||
const cancelExecution = () => {
|
||||
dispatch(cancelRunnerExecution(runnerInfo.cancelTokenUid));
|
||||
};
|
||||
|
||||
const toggleConfigureMode = () => {
|
||||
dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tagsEnabled: false }));
|
||||
setConfigureMode(!configureMode);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(tagsEnabled) {
|
||||
setConfigureMode(false);
|
||||
}
|
||||
}, [tagsEnabled]);
|
||||
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);
|
||||
const passedRequests = items.filter(allTestsPassed);
|
||||
const failedRequests = items.filter(anyTestFailed);
|
||||
@@ -142,59 +196,104 @@ export default function RunnerResults({ collection }) {
|
||||
|
||||
if (!items || !items.length) {
|
||||
return (
|
||||
<StyledWrapper className="px-4 pb-4">
|
||||
<div className="font-medium mt-6 title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
|
||||
</div>
|
||||
{isCollectionLoading ? <div className='my-1 danger'>Requests in this collection are still loading.</div> : null}
|
||||
<div className="mt-6">
|
||||
<label>Delay (in ms)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="block textbox mt-2 py-5"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={delay}
|
||||
onChange={(e) => setDelay(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<StyledWrapper className="pl-4 overflow-hidden h-full">
|
||||
<div className="flex overflow-hidden max-h-full h-full">
|
||||
<div className={`${configureMode ? 'w-1/2 pr-4' : 'w-full'}`}>
|
||||
<div className="font-medium mt-6 title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
|
||||
{isCollectionLoading && (
|
||||
<span className="ml-2 text-sm text-gray-500">
|
||||
(Loading...)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isCollectionLoading ? <div className='my-1 danger'>Requests in this collection are still loading.</div> : null}
|
||||
<div className="mt-6">
|
||||
<label>Delay (in ms)</label>
|
||||
<input
|
||||
type="number"
|
||||
className="block textbox mt-2 py-5"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={delay}
|
||||
onChange={(e) => setDelay(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tags for the collection run */}
|
||||
<RunnerTags collectionUid={collection.uid} className='mb-6' />
|
||||
{/* Tags for the collection run */}
|
||||
<RunnerTags collectionUid={collection.uid} className='mb-6' />
|
||||
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" disabled={shouldDisableCollectionRun} onClick={runCollection}>
|
||||
Run Collection
|
||||
</button>
|
||||
{/* Configure requests option */}
|
||||
<div className="flex flex-col border-b pb-6 mb-6 border-gray-200 dark:border-gray-700">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
id="filter-config"
|
||||
type="radio"
|
||||
name="filterMode"
|
||||
checked={configureMode}
|
||||
onChange={toggleConfigureMode}
|
||||
/>
|
||||
<label htmlFor="filter-config" className="block font-medium">Configure requests to run</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="submit btn btn-sm btn-close mt-6 ml-3" onClick={resetRunner}>
|
||||
Reset
|
||||
</button>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-sm btn-secondary"
|
||||
disabled={shouldDisableCollectionRun || (configureMode && selectedRequestItems.length === 0) || isCollectionLoading}
|
||||
onClick={runCollection}
|
||||
>
|
||||
{configureMode && selectedRequestItems.length > 0
|
||||
? `Run ${selectedRequestItems.length} Selected Request${selectedRequestItems.length > 1 ? 's' : ''}`
|
||||
: "Run Collection"
|
||||
}
|
||||
</button>
|
||||
|
||||
<button className="submit btn btn-sm btn-close" onClick={resetRunner}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{configureMode && (
|
||||
<div className="w-1/2 border-l border-gray-200 dark:border-gray-700">
|
||||
<RunConfigurationPanel
|
||||
collection={collection}
|
||||
selectedItems={selectedRequestItems}
|
||||
setSelectedItems={setSelectedRequestItems}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 pb-4 flex flex-grow flex-col relative overflow-scroll">
|
||||
<div className="flex flex-row">
|
||||
<div className="font-medium my-6 title flex items-center">
|
||||
<StyledWrapper className="px-4 pb-4 flex flex-grow flex-col relative overflow-auto">
|
||||
<div className="flex items-center my-6 flex-row">
|
||||
<div className="font-medium title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
</div>
|
||||
{runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid && (
|
||||
<button className="btn ml-6 my-4 btn-sm btn-danger" onClick={cancelExecution}>
|
||||
<button className="btn btn-sm btn-danger" onClick={cancelExecution}>
|
||||
Cancel Execution
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-4 h-[calc(100%_-_4.375rem)]">
|
||||
|
||||
<div className="flex gap-4 h-[calc(100vh_-_10rem)] overflow-hidden">
|
||||
<div
|
||||
className="flex flex-col flex-1 overflow-y-auto w-full"
|
||||
className={`flex flex-col overflow-y-auto ${selectedItem || (configureMode && !selectedItem && !runnerInfo.status === 'running') ? 'w-1/2' : 'w-full'}`}
|
||||
ref={runnerBodyRef}
|
||||
>
|
||||
<div className="pb-2 font-medium test-summary">
|
||||
@@ -214,57 +313,59 @@ export default function RunnerResults({ collection }) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{runnerInfo?.statusText ?
|
||||
{runnerInfo?.statusText ?
|
||||
<div className="pb-2 font-medium danger">
|
||||
{runnerInfo?.statusText}
|
||||
</div>
|
||||
: null}
|
||||
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{allTestsPassed(item) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{item.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
:null}
|
||||
{anyTestFailed(item) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
:null}
|
||||
</span>
|
||||
<span
|
||||
className={`mr-1 ml-2 ${item.status == 'skipped' ? 'skipped-request' : anyTestFailed(item) ? 'danger' : ''}`}
|
||||
>
|
||||
{item.displayName}
|
||||
</span>
|
||||
{item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
|
||||
) : item.responseReceived?.status ? (
|
||||
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
<span className="mr-1">{item.responseReceived?.status}</span>
|
||||
-
|
||||
<span>{item.responseReceived?.statusText}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
(request failed)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{tagsEnabled && areTagsAdded && item?.tags?.length > 0 && (
|
||||
<div className="pl-7 text-xs text-gray-500">
|
||||
Tags: {item.tags.filter(t => tags.include.includes(t)).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{item.status == 'error' ? <div className="error-message pl-8 pt-2 text-xs">{item.error}</div> : null}
|
||||
: null}
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.preRequestTestResults
|
||||
? item.preRequestTestResults.map((result) => (
|
||||
{/* Items list */}
|
||||
<div className="overflow-y-auto flex-1">
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div key={item.uid}>
|
||||
<div className="item-path mt-2">
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{allTestsPassed(item) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{item.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{anyTestFailed(item) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
</span>
|
||||
<span
|
||||
className={`mr-1 ml-2 ${item.status == 'skipped' ? 'skipped-request' : anyTestFailed(item) ? 'danger' : ''}`}
|
||||
>
|
||||
{item.displayName}
|
||||
</span>
|
||||
{item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? (
|
||||
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
|
||||
) : item.responseReceived?.status ? (
|
||||
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
<span className="mr-1">{item.responseReceived?.status}</span>
|
||||
-
|
||||
<span>{item.responseReceived?.statusText}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}>
|
||||
(request failed)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{tagsEnabled && areTagsAdded && item?.tags?.length > 0 && (
|
||||
<div className="pl-7 text-xs text-gray-500">
|
||||
Tags: {item.tags.filter(t => tags.include.includes(t)).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{item.status == 'error' ? <div className="error-message pl-8 pt-2 text-xs">{item.error}</div> : null}
|
||||
|
||||
<ul className="pl-8">
|
||||
{item.preRequestTestResults
|
||||
? item.preRequestTestResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -282,9 +383,9 @@ export default function RunnerResults({ collection }) {
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.postResponseTestResults
|
||||
? item.postResponseTestResults.map((result) => (
|
||||
: null}
|
||||
{item.postResponseTestResults
|
||||
? item.postResponseTestResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -302,9 +403,9 @@ export default function RunnerResults({ collection }) {
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.testResults
|
||||
? item.testResults.map((result) => (
|
||||
: null}
|
||||
{item.testResults
|
||||
? item.testResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
@@ -322,30 +423,32 @@ export default function RunnerResults({ collection }) {
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.assertionResults?.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
: null}
|
||||
{item.assertionResults?.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{runnerInfo.status === 'ended' ? (
|
||||
<div className="mt-2 mb-4">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runAgain}>
|
||||
@@ -366,15 +469,15 @@ export default function RunnerResults({ collection }) {
|
||||
<div className="flex items-center mb-4 font-medium">
|
||||
<span className="mr-2">{selectedItem.displayName}</span>
|
||||
<span>
|
||||
{allTestsPassed(selectedItem) ?
|
||||
{allTestsPassed(selectedItem) ?
|
||||
<IconCircleCheck className="test-success" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{anyTestFailed(selectedItem) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
: null}
|
||||
{anyTestFailed(selectedItem) ?
|
||||
<IconCircleX className="test-failure" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
{selectedItem.status === 'skipped' ?
|
||||
<IconCircleOff className="skipped-request" size={20} strokeWidth={1.5} />
|
||||
: null}
|
||||
: null}
|
||||
</span>
|
||||
</div>
|
||||
<ResponsePane item={selectedItem} collection={collection} />
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.tooltip-mod {
|
||||
font-size: 11px !important;
|
||||
width: 150px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const SensitiveFieldWarning = ({ fieldName, warningMessage }) => {
|
||||
const tooltipId = `sensitive-field-warning-${fieldName}`;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<span className="ml-2 flex items-center">
|
||||
<IconAlertTriangle id={tooltipId} className="text-amber-600 cursor-pointer" size={20} />
|
||||
<Tooltip
|
||||
anchorId={tooltipId}
|
||||
className="tooltip-mod max-w-lg"
|
||||
content={
|
||||
<div>
|
||||
<p>
|
||||
<span>{warningMessage}</span>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SensitiveFieldWarning;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import { IconDownload } from '@tabler/icons';
|
||||
import { IconDownload, IconLoader2 } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Bruno from 'components/Bruno';
|
||||
import exportBrunoCollection from 'utils/collections/export';
|
||||
@@ -8,10 +8,12 @@ import exportPostmanCollection from 'utils/exporters/postman-collection';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
import { findCollectionByUid, areItemsLoading } from 'utils/collections/index';
|
||||
|
||||
const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const isCollectionLoading = areItemsLoading(collection);
|
||||
|
||||
const handleExportBrunoCollection = () => {
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
|
||||
@@ -35,23 +37,49 @@ const ShareCollection = ({ onClose, collectionUid }) => {
|
||||
>
|
||||
<StyledWrapper className="flex flex-col h-full w-[500px]">
|
||||
<div className="space-y-2">
|
||||
<div className="flex border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-500/10 items-center p-3 rounded-lg transition-colors cursor-pointer" onClick={handleExportBrunoCollection}>
|
||||
<div
|
||||
className={`flex border border-gray-200 dark:border-gray-600 items-center p-3 rounded-lg transition-colors ${
|
||||
isCollectionLoading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-500/10 cursor-pointer'
|
||||
}`}
|
||||
onClick={isCollectionLoading ? undefined : handleExportBrunoCollection}
|
||||
>
|
||||
<div className="mr-3 p-1 rounded-full">
|
||||
<Bruno width={28} />
|
||||
{isCollectionLoading ? (
|
||||
<IconLoader2 size={28} className="animate-spin" />
|
||||
) : (
|
||||
<Bruno width={28} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">Bruno Collection</div>
|
||||
<div className="text-xs">Export in Bruno format</div>
|
||||
<div className="text-xs">
|
||||
{isCollectionLoading ? 'Loading collection...' : 'Export in Bruno format'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex border border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-500/10 items-center p-3 rounded-lg transition-colors cursor-pointer" onClick={handleExportPostmanCollection}>
|
||||
<div
|
||||
className={`flex border border-gray-200 dark:border-gray-600 items-center p-3 rounded-lg transition-colors ${
|
||||
isCollectionLoading
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-500/10 cursor-pointer'
|
||||
}`}
|
||||
onClick={isCollectionLoading ? undefined : handleExportPostmanCollection}
|
||||
>
|
||||
<div className="mr-3 p-1 rounded-full">
|
||||
<IconDownload size={28} strokeWidth={1} className="" />
|
||||
{isCollectionLoading ? (
|
||||
<IconLoader2 size={28} className="animate-spin" />
|
||||
) : (
|
||||
<IconDownload size={28} strokeWidth={1} className="" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">Postman Collection</div>
|
||||
<div className="text-xs">Export in Postman format</div>
|
||||
<div className="text-xs">
|
||||
{isCollectionLoading ? 'Loading collection...' : 'Export in Postman format'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index';
|
||||
import { getLanguages } from 'utils/codegenerator/targets';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
import { getAllVariables, getGlobalEnvironmentVariables } from 'utils/collections/index';
|
||||
import { resolveInheritedAuth } from './utils/auth-utils';
|
||||
|
||||
const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
@@ -37,12 +37,11 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => {
|
||||
const requestUrl =
|
||||
get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
|
||||
const variables = getAllVariables(collection, item);
|
||||
|
||||
const interpolatedUrl = interpolateUrl({
|
||||
url: requestUrl,
|
||||
globalEnvironmentVariables,
|
||||
envVars,
|
||||
runtimeVariables: collection.runtimeVariables,
|
||||
processEnvVars: collection.processEnvVariables
|
||||
variables
|
||||
});
|
||||
|
||||
// interpolate the path params
|
||||
|
||||
@@ -69,24 +69,3 @@ export const interpolateBody = (body, variables = {}) => {
|
||||
|
||||
return interpolatedBody;
|
||||
};
|
||||
|
||||
export const createVariablesObject = ({
|
||||
globalEnvironmentVariables = {},
|
||||
collectionVars = {},
|
||||
allVariables = {},
|
||||
collection = {},
|
||||
runtimeVariables = {},
|
||||
processEnvVars = {}
|
||||
}) => {
|
||||
return {
|
||||
...globalEnvironmentVariables,
|
||||
...allVariables,
|
||||
...collectionVars,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
import { getAuthHeaders } from 'utils/codegenerator/auth';
|
||||
import { getAllVariables, getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import { interpolateHeaders, interpolateBody, createVariablesObject } from './interpolation';
|
||||
import { interpolateHeaders, interpolateBody } from './interpolation';
|
||||
|
||||
// Merge headers from collection, folders, and request
|
||||
const mergeHeaders = (collection, request, requestTreePath) => {
|
||||
@@ -46,17 +46,7 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
|
||||
// Get HTTPSnippet dynamically so mocks can be applied in tests
|
||||
const { HTTPSnippet } = require('httpsnippet');
|
||||
|
||||
const allVariables = getAllVariables(collection, item);
|
||||
|
||||
// Create variables object for interpolation
|
||||
const variables = createVariablesObject({
|
||||
globalEnvironmentVariables: collection.globalEnvironmentVariables || {},
|
||||
collectionVars: collection.collectionVars || {},
|
||||
allVariables,
|
||||
collection,
|
||||
runtimeVariables: collection.runtimeVariables || {},
|
||||
processEnvVars: collection.processEnvVariables || {}
|
||||
});
|
||||
const variables = getAllVariables(collection, item);
|
||||
|
||||
const request = item.request;
|
||||
|
||||
|
||||
@@ -46,7 +46,10 @@ jest.mock('utils/codegenerator/auth', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('utils/collections/index', () => ({
|
||||
getAllVariables: jest.fn(() => ({
|
||||
getAllVariables: jest.fn((collection) => ({
|
||||
...collection?.globalEnvironmentVariables,
|
||||
...collection?.runtimeVariables,
|
||||
...collection?.processEnvVariables,
|
||||
baseUrl: 'https://api.example.com',
|
||||
apiKey: 'secret-key-123',
|
||||
userId: '12345'
|
||||
|
||||
@@ -61,13 +61,14 @@ const Collection = ({ collection, searchText }) => {
|
||||
};
|
||||
|
||||
const ensureCollectionIsMounted = () => {
|
||||
if (collection.mountStatus === 'unmounted') {
|
||||
dispatch(mountCollection({
|
||||
collectionUid: collection.uid,
|
||||
collectionPathname: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig
|
||||
}));
|
||||
if(collection.mountStatus === 'mounted'){
|
||||
return;
|
||||
}
|
||||
dispatch(mountCollection({
|
||||
collectionUid: collection.uid,
|
||||
collectionPathname: collection.pathname,
|
||||
brunoConfig: collection.brunoConfig
|
||||
}));
|
||||
}
|
||||
|
||||
const hasSearchText = searchText && searchText?.trim()?.length;
|
||||
@@ -269,6 +270,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
ensureCollectionIsMounted();
|
||||
handleRun();
|
||||
}}
|
||||
>
|
||||
@@ -287,6 +289,7 @@ const Collection = ({ collection, searchText }) => {
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
ensureCollectionIsMounted();
|
||||
setShowShareCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -5,12 +5,12 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
height: 22px;
|
||||
padding: 0 1rem;
|
||||
height: 1.5rem;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
border-top: 1px solid ${(props) => props.theme.statusBar.border};
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
font-size: 12px;
|
||||
color: ${(props) => props.theme.statusBar.color};
|
||||
font-size: 0.75rem;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
@@ -32,9 +32,7 @@ const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 4px;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
@@ -43,13 +41,11 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
gap: 0.25rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.console-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -66,17 +62,13 @@ const StyledWrapper = styled.div`
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: ${(props) => props.theme.sidebar.dragbar};
|
||||
margin: 0 8px;
|
||||
opacity: 0.3;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.status-bar-version {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -71,18 +71,6 @@ const StatusBar = () => {
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
<ToolHint text="Cookies" toolhintId="Cookies" place="top" offset={10}>
|
||||
<button
|
||||
className="status-bar-button"
|
||||
data-trigger="cookies"
|
||||
onClick={() => setCookiesOpen(true)}
|
||||
tabIndex={0}
|
||||
aria-label="Open Cookies Settings"
|
||||
>
|
||||
<IconCookie size={16} strokeWidth={1.5} aria-hidden="true" />
|
||||
</button>
|
||||
</ToolHint>
|
||||
|
||||
<ToolHint text="Notifications" toolhintId="Notifications" place="top" offset={10}>
|
||||
<div className="status-bar-button">
|
||||
<Notifications />
|
||||
@@ -92,7 +80,20 @@ const StatusBar = () => {
|
||||
</div>
|
||||
|
||||
<div className="status-bar-section">
|
||||
<div className="status-bar-group">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className="status-bar-button"
|
||||
data-trigger="cookies"
|
||||
onClick={() => setCookiesOpen(true)}
|
||||
tabIndex={0}
|
||||
aria-label="Open Cookies"
|
||||
>
|
||||
<div className="console-button-content">
|
||||
<IconCookie size={16} strokeWidth={1.5} aria-hidden="true" />
|
||||
<span className="console-label">Cookies</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`status-bar-button ${errorCount > 0 ? 'has-errors' : ''}`}
|
||||
data-trigger="dev-tools"
|
||||
@@ -100,13 +101,13 @@ const StatusBar = () => {
|
||||
tabIndex={0}
|
||||
aria-label={`Open Dev Tools${errorCount > 0 ? ` (${errorCount} errors)` : ''}`}
|
||||
>
|
||||
<div className="console-button-content">
|
||||
<IconTool size={16} strokeWidth={1.5} aria-hidden="true" />
|
||||
<span className="console-label">Dev Tools</span>
|
||||
{errorCount > 0 && (
|
||||
<span className="error-count-inline">{errorCount}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="console-button-content">
|
||||
<IconTool size={16} strokeWidth={1.5} aria-hidden="true" />
|
||||
<span className="console-label">Dev Tools</span>
|
||||
{errorCount > 0 && (
|
||||
<span className="error-count-inline">{errorCount}</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="status-bar-divider"></div>
|
||||
|
||||
@@ -6,7 +6,7 @@ const StyledWrapper = styled.div`
|
||||
display: grid;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
padding: 0 1px;
|
||||
padding: 0 1.5px;
|
||||
|
||||
// for icon hover
|
||||
position: inherit;
|
||||
|
||||
@@ -89,7 +89,7 @@ const VariablesEditor = ({ collection }) => {
|
||||
const reactInspectorTheme = storedTheme === 'light' ? 'chromeLight' : 'chromeDark';
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 py-4 overflow-scroll">
|
||||
<StyledWrapper className="px-4 py-4 overflow-auto">
|
||||
<RuntimeVariables collection={collection} theme={reactInspectorTheme} />
|
||||
<EnvVariables collection={collection} theme={reactInspectorTheme} />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user