Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 96 additions & 84 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions packages/bruno-app/src/components/EditableTable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const TableRow = React.memo(
const rowIndex = Number(rest['data-item-index']);
const { reorderable, reorderableRowCount, isLastEmptyRow, dragOverRow, onDragStart, onDragOver, onDrop, onDragEnd, onDragLeave, keyColumn } = context;
const isEmpty = isLastEmptyRow(item, rowIndex);
const canDrag = reorderable && !isEmpty && rowIndex < reorderableRowCount;
const canDrag = reorderable && !isEmpty && rowIndex < reorderableRowCount && !item.readOnly;
const isDragOver = canDrag && dragOverRow === rowIndex;
const existingClass = rest.className || '';
const className = isDragOver ? `${existingClass} drag-over`.trim() : existingClass;
Expand Down Expand Up @@ -387,7 +387,7 @@ const EditableTable = ({

const itemContent = useCallback((rowIndex, row) => {
const isEmpty = isLastEmptyRow(row, rowIndex);
const canDrag = reorderable && !isEmpty && rowIndex < reorderableRowCount;
const canDrag = reorderable && !isEmpty && rowIndex < reorderableRowCount && !row.readOnly;

return (
<>
Expand All @@ -414,7 +414,7 @@ const EditableTable = ({
className="mousetrap"
data-testid="column-checkbox"
checked={row[checkboxKey] ?? true}
disabled={disableCheckbox}
disabled={disableCheckbox || row.readOnly}
onChange={(e) => handleCheckboxChange(row.uid, e.target.checked)}
/>
)}
Expand All @@ -427,7 +427,7 @@ const EditableTable = ({
))}
{showDelete && (
<td>
{!isEmpty && (
{!isEmpty && !row.readOnly && (
<button
data-testid="column-delete"
onClick={() => handleRemoveRow(row.uid)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ const Wrapper = styled.div`
}
}

.bulk-edit-bar {
.actions-bar {
position: sticky;
bottom: 0;
background: ${(props) => props.theme.bg};
padding-top: 8px;
padding-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}

input[type='text'] {
Expand All @@ -34,6 +37,11 @@ const Wrapper = styled.div`
position: relative;
top: 1px;
}

.read-only {
opacity: 0.6;
cursor: not-allowed;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
`;

export default Wrapper;
183 changes: 161 additions & 22 deletions packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,126 @@
import React, { useState, useCallback, useRef } from 'react';
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import get from 'lodash/get';
import { useDispatch, useSelector } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { moveRequestHeader, setRequestHeaders } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { updateTableColumnWidths } from 'providers/ReduxStore/slices/tabs';
import { updateTableColumnWidths, updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
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 '../../BulkEditor';
import { headerNameRegex, headerValueRegex } from 'utils/common/regex';
import useLocalStorage from 'hooks/useLocalStorage';
import { usePersistedState } from 'hooks/usePersistedState';
import { useTrackScroll } from 'hooks/useTrackScroll';
import { findEnvironmentInCollection } from 'utils/collections';
import { resolveInheritedAuth } from 'utils/auth';
import { dryRunHttpRequest } from 'utils/network';
import { IconEye, IconEyeOff } from '@tabler/icons';

const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);

const RequestHeaders = ({ item, collection, addHeaderText }) => {
const RequestHeaders = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
const [showDynamicHeaders, setShowDynamicHeaders] = useLocalStorage('bruno.request.showDynamicHeaders', false);
const [showAuthValue, setShowAuthValue] = useState(false);

// Calculate dynamic headers via dry-run
const activeRequest = item.draft ? item.draft.request : item.request;

const ownHeaderNames = useMemo(
() => new Set((headers || []).map((h) => h.name.trim().toLowerCase())),
[headers]
);

const [dryRunHeaders, setDryRunHeaders] = useState({});

const bodyMode = activeRequest.body?.mode;
const itemAuthStr = JSON.stringify(activeRequest.auth || {});
const collectionAuthStr = JSON.stringify(collection?.root?.request?.auth || {});
const activeEnvironmentUid = collection?.activeEnvironmentUid;

const authHeaderNames = useMemo(() => {
const effectiveAuth = resolveInheritedAuth(item, collection)?.auth || {};
const names = new Set(['authorization', 'x-wsse']);
if (effectiveAuth.mode === 'apikey' && effectiveAuth.apikey?.placement === 'header' && effectiveAuth.apikey?.key) {
names.add(effectiveAuth.apikey.key.toLowerCase());
}
return names;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}, [item, collection]);

useEffect(() => {
let isMounted = true;
const environment = findEnvironmentInCollection(collection, activeEnvironmentUid);

dryRunHttpRequest(item, collection, environment, collection.runtimeVariables)
.then((res) => {
if (!isMounted) return;
if (res?.error || !res?.headers) {
setDryRunHeaders({});
return;
}
setDryRunHeaders(res.headers);
})
.catch((error) => {
console.error(error);
if (isMounted) {
setDryRunHeaders({});
}
});

return () => { isMounted = false; };
}, [item.uid, bodyMode, itemAuthStr, collectionAuthStr, activeEnvironmentUid]);

const dynamicHeaders = useMemo(() => {
return Object.entries(dryRunHeaders)
.filter(([name]) => !ownHeaderNames.has(name.trim().toLowerCase()))
.map(([name, value], index) => {
const trimmedName = name.trim();
let displayValue = typeof value === 'string' ? value.trim() : String(value).trim();
const lowerName = trimmedName.toLowerCase();

// Obscure timestamp, size, and dynamic signature headers that change on every execution
if (['x-amz-date', 'date', 'content-length', 'request-start-time'].includes(lowerName)) {
displayValue = '<calculated at runtime>';
} else if (authHeaderNames.has(lowerName)) {
if (!showAuthValue) {
if (displayValue.includes('Credential=')) {
// AWSv4 authorization contains the signature and date which changes every execution
displayValue = '<calculated at runtime>';
} else {
// Obscure normal auth values with bullets if it has a bearer/basic prefix
const parts = displayValue.split(' ');
if (parts.length > 1) {
displayValue = parts[0] + ' ' + '*'.repeat(16);
} else {
displayValue = '*'.repeat(16);
}
}
}
}

return {
uid: `dynamic-${trimmedName}-${index}`,
name: trimmedName,
value: displayValue,
readOnly: true,
isDynamic: true
};
});
}, [dryRunHeaders, ownHeaderNames, authHeaderNames, showAuthValue]);

const rows = useMemo(() => {
return showDynamicHeaders ? [...dynamicHeaders, ...(headers || [])] : headers || [];
}, [showDynamicHeaders, dynamicHeaders, headers]);

const wrapperRef = useRef(null);
const [scroll, setScroll] = usePersistedState({ key: `request-headers-scroll-${item.uid}`, default: 0 });
useTrackScroll({ ref: wrapperRef, selector: '.flex-boundary', onChange: setScroll, initialValue: scroll });
Expand All @@ -40,18 +137,21 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
const handleRun = () => dispatch(sendRequest(item, collection.uid));

const handleHeadersChange = useCallback((updatedHeaders) => {
const ownUpdatedHeaders = updatedHeaders.filter((h) => !h.isDynamic);
dispatch(setRequestHeaders({
collectionUid: collection.uid,
itemUid: item.uid,
headers: updatedHeaders
headers: ownUpdatedHeaders
}));
}, [dispatch, collection.uid, item.uid]);

const handleHeaderDrag = useCallback(({ updateReorderedItem }) => {
const validUids = updateReorderedItem.filter((uid) => !uid.startsWith('dynamic-'));

dispatch(moveRequestHeader({
collectionUid: collection.uid,
itemUid: item.uid,
updateReorderedItem
updateReorderedItem: validUids
}));
}, [dispatch, collection.uid, item.uid]);

Expand Down Expand Up @@ -82,10 +182,11 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
isKeyField: true,
placeholder: 'Name',
width: '30%',
render: ({ value, onChange }) => (
render: ({ row, value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
readOnly={row.readOnly}
onSave={onSave}
onChange={(newValue) => onChange(newValue.replace(/[\r\n]/g, ''))}
autocomplete={headerAutoCompleteList}
Expand All @@ -100,19 +201,47 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
key: 'value',
name: 'Value',
placeholder: 'Value',
render: ({ value, onChange }) => (
<SingleLineEditor
value={value || ''}
theme={storedTheme}
onSave={onSave}
onChange={onChange}
onRun={handleRun}
autocomplete={MimeTypes}
collection={collection}
item={item}
placeholder={!value ? 'Value' : ''}
/>
)
render: ({ row, value, onChange }) => {
const isAuthHeader = row.name && authHeaderNames.has(row.name.toLowerCase()) && row.isDynamic;

return (
<div className={`relative flex w-full items-center ${isAuthHeader ? 'group' : ''}`}>
<div className={`w-full ${isAuthHeader ? 'transition-opacity duration-200 group-hover:opacity-15' : ''}`}>
<SingleLineEditor
value={value || ''}
theme={storedTheme}
readOnly={row.readOnly}
onSave={onSave}
onChange={onChange}
onRun={handleRun}
autocomplete={MimeTypes}
collection={collection}
item={item}
placeholder={!value ? 'Value' : ''}
/>
</div>
{isAuthHeader && (
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity z-10">
<div
className="cursor-pointer text-link text-xs whitespace-nowrap px-2 py-1 rounded"
onClick={() => dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: 'auth' }))}
data-testid="go-to-authorization"
>
Go to Authorization
</div>
<div
className="cursor-pointer text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 p-1 rounded"
onClick={() => setShowAuthValue(!showAuthValue)}
title={showAuthValue ? 'Hide value' : 'Show value'}
data-testid="reveal-auth-value"
>
{showAuthValue ? <IconEyeOff size={16} strokeWidth={1.5} /> : <IconEye size={16} strokeWidth={1.5} />}
</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
)}
</div>
);
}
}
];

Expand Down Expand Up @@ -141,7 +270,7 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
<EditableTable
tableId="request-headers"
columns={columns}
rows={headers || []}
rows={rows}
onChange={handleHeadersChange}
defaultRow={defaultRow}
getRowError={getRowError}
Expand All @@ -151,8 +280,18 @@ const RequestHeaders = ({ item, collection, addHeaderText }) => {
columnWidths={headersWidths}
onColumnWidthsChange={(widths) => handleColumnWidthsChange('request-headers', widths)}
/>
<div className="bulk-edit-bar flex justify-end mt-2">
<button className="btn-action text-link select-none" data-testid="bulk-edit-toggle" onClick={toggleBulkEditMode}>
<div className="actions-bar flex justify-between gap-2 mt-2">
{dynamicHeaders.length > 0 && (
<button
className="btn-action text-link select-none flex items-center gap-1"
onClick={() => setShowDynamicHeaders(!showDynamicHeaders)}
data-testid="dynamic-header-toggle"
>
{showDynamicHeaders ? <IconEye size={16} strokeWidth={1.5} /> : <IconEyeOff size={16} strokeWidth={1.5} />}
<span>{showDynamicHeaders ? `Hide ${dynamicHeaders.length} dynamic headers` : `${dynamicHeaders.length} Hidden headers`}</span>
</button>
)}
<button className="btn-action text-link select-none ml-auto" data-testid="bulk-edit-toggle" onClick={toggleBulkEditMode}>
Bulk Edit
</button>
</div>
Expand Down
11 changes: 11 additions & 0 deletions packages/bruno-app/src/utils/network/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ const sendHttpRequest = async (item, collection, environment, runtimeVariables)
});
};

export const dryRunHttpRequest = async (item, collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;

ipcRenderer
.invoke('dry-run-http-request', item, collection, environment, runtimeVariables)
.then(resolve)
.catch(reject);
});
};

export const sendCollectionOauth2Request = async (collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
Expand Down
Loading