mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
feat: Tagging requests and filtering collection runs using tags
This commit is contained in:
committed by
lohit-bruno
parent
ecc6c1604c
commit
3803576aa4
@@ -19,6 +19,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import Tags from 'components/RequestPane/Tags/index';
|
||||
|
||||
const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -101,6 +102,9 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
|
||||
case 'docs': {
|
||||
return <Documentation item={item} collection={collection} />;
|
||||
}
|
||||
case 'tags': {
|
||||
return <Tags item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -152,6 +156,9 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
<div className={getTabClassname('tags')} role="tab" onClick={() => selectTab('tags')}>
|
||||
Tags
|
||||
</div>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
<section className="flex w-full mt-5 flex-1 relative">
|
||||
|
||||
@@ -18,6 +18,7 @@ import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import { useEffect } from 'react';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import Settings from 'components/RequestPane/Settings';
|
||||
import Tags from 'components/RequestPane/Tags/index';
|
||||
|
||||
const HttpRequestPane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -65,6 +66,9 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
case 'settings': {
|
||||
return <Settings item={item} collection={collection} />;
|
||||
}
|
||||
case 'tags': {
|
||||
return <Tags item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -165,6 +169,9 @@ const HttpRequestPane = ({ item, collection }) => {
|
||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||
Settings
|
||||
</div>
|
||||
<div className={getTabClassname('tags')} role="tab" onClick={() => selectTab('tags')}>
|
||||
Tags
|
||||
</div>
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<RequestBodyMode item={item} collection={collection} />
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
input[type='text'] {
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid ${(props) => props.theme.text};
|
||||
border-radius: 5px;
|
||||
padding-inline: 5px;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { IconX } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const TagList = ({ tags, onTagRemove, onTagAdd }) => {
|
||||
const tagNameRegex = /^[\w-]+$/;
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [text, setText] = useState('');
|
||||
|
||||
const handleChange = (e) => {
|
||||
setText(e.target.value);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.code == 'Escape') {
|
||||
setText('');
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
if (e.code !== 'Enter' && e.code !== 'Space') {
|
||||
return;
|
||||
}
|
||||
if (!tagNameRegex.test(text)) {
|
||||
toast.error('Tags must only contain alpha-numeric characters, "-", "_"');
|
||||
return;
|
||||
}
|
||||
if (tags.includes(text)) {
|
||||
toast.error(`Tag "${text}" already exists`);
|
||||
return;
|
||||
}
|
||||
onTagAdd(text);
|
||||
setText('');
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-wrap gap-2 mt-1">
|
||||
<ul className="flex flex-wrap gap-1">
|
||||
{tags && tags.length
|
||||
? tags.map((_tag) => (
|
||||
<li key={_tag}>
|
||||
<span>{_tag}</span>
|
||||
<button tabIndex={-1} onClick={() => onTagRemove(_tag)}>
|
||||
<IconX strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
</ul>
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Space or Enter to add tag"
|
||||
value={text}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<button className="text-link select-none" onClick={() => setIsEditing(true)}>
|
||||
+ Add
|
||||
</button>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagList;
|
||||
43
packages/bruno-app/src/components/RequestPane/Tags/index.js
Normal file
43
packages/bruno-app/src/components/RequestPane/Tags/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'github-markdown-css/github-markdown.css';
|
||||
import get from 'lodash/get';
|
||||
import { addRequestTag, deleteRequestTag } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import TagList from './TagList/TagList';
|
||||
|
||||
const Tags = ({ item, collection }) => {
|
||||
const tags = item.draft ? get(item, 'draft.request.tags') : get(item, 'request.tags');
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleAdd = (_tag) => {
|
||||
dispatch(
|
||||
addRequestTag({
|
||||
tag: _tag,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemove = (_tag) => {
|
||||
dispatch(
|
||||
deleteRequestTag({
|
||||
tag: _tag,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TagList tags={tags} onTagRemove={handleRemove} onTagAdd={handleAdd} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tags;
|
||||
@@ -9,6 +9,7 @@ import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, Ic
|
||||
import ResponsePane from './ResponsePane';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
import TagList from 'components/RequestPane/Tags/TagList/TagList';
|
||||
|
||||
const getDisplayName = (fullPath, pathname, name = '') => {
|
||||
let relativePath = path.relative(fullPath, pathname);
|
||||
@@ -42,6 +43,8 @@ export default function RunnerResults({ collection }) {
|
||||
const dispatch = useDispatch();
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [delay, setDelay] = useState(null);
|
||||
const [tags, setTags] = useState({ include: [], exclude: [] });
|
||||
const [tagsEnabled, setTagsEnabled] = useState(false);
|
||||
|
||||
// ref for the runner output body
|
||||
const runnerBodyRef = useRef();
|
||||
@@ -88,11 +91,19 @@ export default function RunnerResults({ collection }) {
|
||||
.filter(Boolean);
|
||||
|
||||
const runCollection = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay)));
|
||||
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags));
|
||||
};
|
||||
|
||||
const runAgain = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive, Number(delay)));
|
||||
dispatch(
|
||||
runCollectionFolder(
|
||||
collection.uid,
|
||||
runnerInfo.folderUid,
|
||||
runnerInfo.isRecursive,
|
||||
Number(delay),
|
||||
tagsEnabled && tags
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const resetRunner = () => {
|
||||
@@ -140,6 +151,37 @@ export default function RunnerResults({ collection }) {
|
||||
onChange={(e) => setDelay(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-col">
|
||||
<div className="flex gap-2">
|
||||
<label className="block font-medium">Filter requests with tags</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
checked={tagsEnabled}
|
||||
onChange={() => setTagsEnabled(!tagsEnabled)}
|
||||
/>
|
||||
</div>
|
||||
{tagsEnabled && (
|
||||
<div className="flex p-4 gap-4 max-w-xl justify-between">
|
||||
<div className="w-1/2">
|
||||
<span>Included tags:</span>
|
||||
<TagList
|
||||
tags={tags.include}
|
||||
onTagAdd={(tag) => setTags({ ...tags, include: [...tags.include, tag] })}
|
||||
onTagRemove={(tag) => setTags({ ...tags, include: tags.include.filter((t) => t !== tag) })}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<span>Excluded tags:</span>
|
||||
<TagList
|
||||
tags={tags.exclude}
|
||||
onTagAdd={(tag) => setTags({ ...tags, exclude: [...tags.exclude, tag] })}
|
||||
onTagRemove={(tag) => setTags({ ...tags, exclude: tags.exclude.filter((t) => t !== tag) })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
|
||||
Run Collection
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { uuid } from 'utils/common';
|
||||
import Modal from 'components/Modal';
|
||||
@@ -8,12 +8,15 @@ import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/act
|
||||
import { flattenItems } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { areItemsLoading } from 'utils/collections';
|
||||
import TagList from 'components/RequestPane/Tags/TagList/TagList';
|
||||
|
||||
const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const collection = useSelector(state => state.collections.collections?.find(c => c.uid === collectionUid));
|
||||
const isCollectionRunInProgress = collection?.runnerResult?.info?.status && (collection?.runnerResult?.info?.status !== 'ended');
|
||||
const [tags, setTags] = useState({ include: [], exclude: [] });
|
||||
const [tagsEnabled, setTagsEnabled] = useState(false);
|
||||
|
||||
const onSubmit = (recursive) => {
|
||||
dispatch(
|
||||
@@ -24,7 +27,7 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
})
|
||||
);
|
||||
if (!isCollectionRunInProgress) {
|
||||
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive));
|
||||
dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive, 0, tagsEnabled && tags));
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
@@ -71,6 +74,39 @@ const RunCollectionItem = ({ collectionUid, item, onClose }) => {
|
||||
<div className={isFolderLoading ? "mb-2" : "mb-8"}>This will run all the requests in this folder and all its subfolders.</div>
|
||||
{isFolderLoading ? <div className='mb-8 warning'>Requests in this folder are still loading.</div> : null}
|
||||
{isCollectionRunInProgress ? <div className='mb-6 warning'>A Collection Run is already in progress.</div> : null}
|
||||
|
||||
<div className="mb-8 flex flex-col">
|
||||
<div className="flex gap-2">
|
||||
<label className="block font-medium">Filter requests with tags</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
checked={tagsEnabled}
|
||||
onChange={() => setTagsEnabled(!tagsEnabled)}
|
||||
/>
|
||||
</div>
|
||||
{tagsEnabled && (
|
||||
<div className="flex p-4 gap-4 max-w-xl justify-between">
|
||||
<div className="w-1/2">
|
||||
<span>Included tags:</span>
|
||||
<TagList
|
||||
tags={tags.include}
|
||||
onTagAdd={(tag) => setTags({ ...tags, include: [...tags.include, tag] })}
|
||||
onTagRemove={(tag) => setTags({ ...tags, include: tags.include.filter((t) => t !== tag) })}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<span>Excluded tags:</span>
|
||||
<TagList
|
||||
tags={tags.exclude}
|
||||
onTagAdd={(tag) => setTags({ ...tags, exclude: [...tags.exclude, tag] })}
|
||||
onTagRemove={(tag) => setTags({ ...tags, exclude: tags.exclude.filter((t) => t !== tag) })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end bruno-modal-footer">
|
||||
<span className="mr-3">
|
||||
<button type="button" onClick={onClose} className="btn btn-md btn-close">
|
||||
|
||||
@@ -315,7 +315,7 @@ export const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {
|
||||
cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));
|
||||
};
|
||||
|
||||
export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) => (dispatch, getState) => {
|
||||
export const runCollectionFolder = (collectionUid, folderUid, recursive, delay, tags) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
@@ -354,7 +354,8 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay)
|
||||
environment,
|
||||
collectionCopy.runtimeVariables,
|
||||
recursive,
|
||||
delay
|
||||
delay,
|
||||
tags
|
||||
)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
|
||||
@@ -2339,9 +2339,43 @@ export const collectionsSlice = createSlice({
|
||||
set(folder, 'root.request.auth', {});
|
||||
set(folder, 'root.request.auth.mode', action.payload.mode);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
addRequestTag: (state, action) => {
|
||||
const { tag, collectionUid, itemUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.tags = item.draft.request.tags || [];
|
||||
if (!item.draft.request.tags.includes(tag.trim())) {
|
||||
item.draft.request.tags.push(tag.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteRequestTag: (state, action) => {
|
||||
const { tag, collectionUid, itemUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
item.draft.request.tags = item.draft.request.tags || [];
|
||||
item.draft.request.tags = item.draft.request.tags.filter((t) => t !== tag.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const {
|
||||
@@ -2457,6 +2491,8 @@ export const {
|
||||
collectionGetOauth2CredentialsByUrl,
|
||||
updateFolderAuth,
|
||||
updateFolderAuthMode,
|
||||
addRequestTag,
|
||||
deleteRequestTag
|
||||
} = collectionsSlice.actions;
|
||||
|
||||
export default collectionsSlice.reducer;
|
||||
|
||||
@@ -257,7 +257,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
vars: si.request.vars,
|
||||
assertions: si.request.assertions,
|
||||
tests: si.request.tests,
|
||||
docs: si.request.docs
|
||||
docs: si.request.docs,
|
||||
tags: si.request.tags
|
||||
};
|
||||
|
||||
// Handle auth object dynamically
|
||||
@@ -565,7 +566,8 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
||||
vars: _item.request.vars,
|
||||
assertions: _item.request.assertions,
|
||||
tests: _item.request.tests,
|
||||
docs: _item.request.docs
|
||||
docs: _item.request.docs,
|
||||
tags: _item.request.tags
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const { getRunnerSummary } = require('@usebruno/common/runner');
|
||||
const { exists, isFile, isDirectory } = require('../utils/filesystem');
|
||||
const { runSingleRequest } = require('../runner/run-single-request');
|
||||
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
|
||||
const { isRequestTagsIncluded } = require("@usebruno/common")
|
||||
const makeJUnitOutput = require('../reporters/junit');
|
||||
const makeHtmlOutput = require('../reporters/html');
|
||||
const { rpad } = require('../utils/common');
|
||||
@@ -199,6 +200,14 @@ const builder = async (yargs) => {
|
||||
type:"number",
|
||||
description: "Delay between each requests (in miliseconds)"
|
||||
})
|
||||
.option('tags', {
|
||||
type: 'string',
|
||||
description: 'Tags to include in the run'
|
||||
})
|
||||
.option('exclude-tags', {
|
||||
type: 'string',
|
||||
description: 'Tags to exclude from the run'
|
||||
})
|
||||
.example('$0 run request.bru', 'Run a request')
|
||||
.example('$0 run request.bru --env local', 'Run a request with the environment set to local')
|
||||
.example('$0 run request.bru --env-file env.bru', 'Run a request with the environment from env.bru file')
|
||||
@@ -241,7 +250,11 @@ const builder = async (yargs) => {
|
||||
)
|
||||
.example('$0 run --client-cert-config client-cert-config.json', 'Run a request with Client certificate configurations')
|
||||
.example('$0 run folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.')
|
||||
.example('$0 run --noproxy', 'Run requests with system proxy disabled');
|
||||
.example('$0 run --noproxy', 'Run requests with system proxy disabled')
|
||||
.example(
|
||||
'$0 run folder --tags=hello,world --exclude-tags=skip',
|
||||
'Run only requests with tags "hello" or "world" and exclude any request with tag "skip".'
|
||||
);
|
||||
};
|
||||
|
||||
const handler = async function (argv) {
|
||||
@@ -268,7 +281,9 @@ const handler = async function (argv) {
|
||||
reporterSkipHeaders,
|
||||
clientCertConfig,
|
||||
noproxy,
|
||||
delay
|
||||
delay,
|
||||
tags: includeTags,
|
||||
excludeTags
|
||||
} = argv;
|
||||
const collectionPath = process.cwd();
|
||||
|
||||
@@ -353,7 +368,7 @@ const handler = async function (argv) {
|
||||
if (!match) {
|
||||
console.error(
|
||||
chalk.red(`Overridable environment variable not correct: use name=value - presented: `) +
|
||||
chalk.dim(`${value}`)
|
||||
chalk.dim(`${value}`)
|
||||
);
|
||||
process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_ENV_OVERRIDE);
|
||||
}
|
||||
@@ -389,6 +404,9 @@ const handler = async function (argv) {
|
||||
}
|
||||
options['ignoreTruststore'] = ignoreTruststore;
|
||||
|
||||
includeTags = includeTags ? includeTags.split(',') : [];
|
||||
excludeTags = excludeTags ? excludeTags.split(',') : [];
|
||||
|
||||
if (['json', 'junit', 'html'].indexOf(format) === -1) {
|
||||
console.error(chalk.red(`Format must be one of "json", "junit or "html"`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_OUTPUT_FORMAT);
|
||||
@@ -444,6 +462,10 @@ const handler = async function (argv) {
|
||||
console.error(chalk.red(`Path not found: ${resolvedPath}`));
|
||||
process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
requestItems = requestItems.filter((item) => {
|
||||
return isRequestTagsIncluded(item.tags, includeTags, excludeTags);
|
||||
});
|
||||
}
|
||||
|
||||
requestItems = getCallStack(resolvedPaths, collection, { recursive });
|
||||
|
||||
@@ -64,6 +64,7 @@ const bruToJson = (bru) => {
|
||||
name: _.get(json, 'meta.name'),
|
||||
seq: !_.isNaN(sequence) ? Number(sequence) : 1,
|
||||
settings: _.get(json, 'settings', {}),
|
||||
tags: _.get(json, 'tags', []),
|
||||
request: {
|
||||
method: _.upperCase(_.get(json, 'http.method')),
|
||||
url: _.get(json, 'http.url'),
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { mockDataFunctions } from './utils/faker-functions';
|
||||
export { default as interpolate } from './interpolate';
|
||||
export {default as interpolate} from './interpolate';
|
||||
export {default as isRequestTagsIncluded} from './tags';
|
||||
|
||||
43
packages/bruno-common/src/tags/index.spec.ts
Normal file
43
packages/bruno-common/src/tags/index.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import isRequestTagsIncluded from './index';
|
||||
|
||||
describe('isRequestTagsIncluded', () => {
|
||||
it('should include request when it has an included tag', () => {
|
||||
const requestTags = ['tag1', 'tag2'];
|
||||
const includeTags = ['tag1'];
|
||||
const excludeTags: string[] = [];
|
||||
const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should include request when included tags is empty', () => {
|
||||
const requestTags = ['tag1', 'tag2'];
|
||||
const includeTags: string[] = [];
|
||||
const excludeTags: string[] = [];
|
||||
const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should exclude request when it does not have an included tag', () => {
|
||||
const requestTags = ['tag1'];
|
||||
const includeTags = ['tag2'];
|
||||
const excludeTags: string[] = [];
|
||||
const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should exclude request when it has an excluded tag', () => {
|
||||
const requestTags = ['tag1'];
|
||||
const includeTags: string[] = [];
|
||||
const excludeTags = ['tag1'];
|
||||
const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should exclude request when it has both included and excluded tag', () => {
|
||||
const requestTags = ['tag1', 'tag2'];
|
||||
const includeTags: string[] = ['tag2'];
|
||||
const excludeTags = ['tag1'];
|
||||
const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
13
packages/bruno-common/src/tags/index.ts
Normal file
13
packages/bruno-common/src/tags/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* A request should be included if it has at least one tag that is included and no tags that are excluded
|
||||
* @param requestTags Tags of the request
|
||||
* @param includeTags Tags to include
|
||||
* @param excludeTags Tags to exclude
|
||||
*/
|
||||
export const isRequestTagsIncluded = (requestTags: string[], includeTags: string[], excludeTags: string[]) => {
|
||||
const shouldInclude = includeTags.length === 0 || requestTags.some((tag) => includeTags.includes(tag));
|
||||
const shouldExclude = excludeTags.length > 0 && requestTags.some((tag) => excludeTags.includes(tag));
|
||||
return shouldInclude && !shouldExclude;
|
||||
};
|
||||
|
||||
export default isRequestTagsIncluded;
|
||||
@@ -149,7 +149,8 @@ const bruToJson = (data, parsed = false) => {
|
||||
vars: _.get(json, 'vars', {}),
|
||||
assertions: _.get(json, 'assertions', []),
|
||||
tests: _.get(json, 'tests', ''),
|
||||
docs: _.get(json, 'docs', '')
|
||||
docs: _.get(json, 'docs', ''),
|
||||
tags: _.get(json, 'tags', [])
|
||||
}
|
||||
};
|
||||
|
||||
@@ -215,7 +216,8 @@ const jsonToBru = async (json) => {
|
||||
assertions: _.get(json, 'request.assertions', []),
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
settings: _.get(json, 'settings', {}),
|
||||
docs: _.get(json, 'request.docs', '')
|
||||
docs: _.get(json, 'request.docs', ''),
|
||||
tags: _.get(json, 'request.tags', [])
|
||||
};
|
||||
|
||||
const bru = jsonToBruV2(bruJson);
|
||||
@@ -257,7 +259,8 @@ const jsonToBruViaWorker = async (json) => {
|
||||
assertions: _.get(json, 'request.assertions', []),
|
||||
tests: _.get(json, 'request.tests', ''),
|
||||
settings: _.get(json, 'settings', {}),
|
||||
docs: _.get(json, 'request.docs', '')
|
||||
docs: _.get(json, 'request.docs', ''),
|
||||
tags: _.get(json, 'request.tags', [])
|
||||
};
|
||||
|
||||
const bru = await bruParserWorker?.jsonToBru(bruJson)
|
||||
|
||||
@@ -31,6 +31,7 @@ const { preferencesUtil } = require('../../store/preferences');
|
||||
const { getProcessEnvVars } = require('../../store/process-env');
|
||||
const { getBrunoConfig } = require('../../store/bruno-config');
|
||||
const Oauth2Store = require('../../store/oauth2');
|
||||
const { isRequestTagsIncluded } = require('@usebruno/common');
|
||||
|
||||
const saveCookies = (url, headers) => {
|
||||
if (preferencesUtil.shouldStoreCookies()) {
|
||||
@@ -952,7 +953,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
ipcMain.handle(
|
||||
'renderer:run-collection-folder',
|
||||
async (event, folder, collection, environment, runtimeVariables, recursive, delay) => {
|
||||
async (event, folder, collection, environment, runtimeVariables, recursive, delay, tags) => {
|
||||
const collectionUid = collection.uid;
|
||||
const collectionPath = collection.pathname;
|
||||
const folderUid = folder ? folder.uid : null;
|
||||
@@ -1012,6 +1013,15 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
folderRequests = sortByNameThenSequence(folderRequests)
|
||||
}
|
||||
|
||||
// Filter requests based on tags
|
||||
if (tags && tags.include && tags.exclude) {
|
||||
const includeTags = tags.include ? tags.include : [];
|
||||
const excludeTags = tags.exclude ? tags.exclude : [];
|
||||
folderRequests = folderRequests.filter(({ request }) => {
|
||||
return isRequestTagsIncluded(request.tags, includeTags, excludeTags)
|
||||
});
|
||||
}
|
||||
|
||||
let currentRequestIndex = 0;
|
||||
let nJumps = 0; // count the number of jumps to avoid infinite loops
|
||||
while (currentRequestIndex < folderRequests.length) {
|
||||
|
||||
@@ -4,7 +4,7 @@ const { safeParseJson, outdentString } = require('./utils');
|
||||
|
||||
/**
|
||||
* A Bru file is made up of blocks.
|
||||
* There are two types of blocks
|
||||
* There are three types of blocks
|
||||
*
|
||||
* 1. Dictionary Blocks - These are blocks that have key value pairs
|
||||
* ex:
|
||||
@@ -19,6 +19,13 @@ const { safeParseJson, outdentString } = require('./utils');
|
||||
* "username": "John Nash",
|
||||
* "password": "governingdynamics
|
||||
* }
|
||||
|
||||
* 3. List Blocks - These are blocks that have a list of items
|
||||
* ex:
|
||||
* tags [
|
||||
* regression
|
||||
* smoke-test
|
||||
* ]
|
||||
*
|
||||
*/
|
||||
const grammar = ohm.grammar(`Bru {
|
||||
@@ -59,6 +66,13 @@ const grammar = ohm.grammar(`Bru {
|
||||
textline = textchar*
|
||||
textchar = ~nl any
|
||||
|
||||
// List
|
||||
listend = nl "]"
|
||||
list = st* "[" listitems? listend
|
||||
listitems = (~listend nl)* listitem (~listend stnl* listitem)* (~listend space)*
|
||||
listitem = st* textchar+ st*
|
||||
|
||||
tags = "tags" list
|
||||
meta = "meta" dictionary
|
||||
settings = "settings" dictionary
|
||||
|
||||
@@ -298,6 +312,15 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
assertkey(chars) {
|
||||
return chars.sourceString ? chars.sourceString.trim() : '';
|
||||
},
|
||||
list(_1, _2, listitems, _3) {
|
||||
return listitems.ast.flat()
|
||||
},
|
||||
listitems(_1, listitem, _2, rest, _3) {
|
||||
return [listitem.ast, ...rest.ast]
|
||||
},
|
||||
listitem(_1, textchar, _2) {
|
||||
return textchar.sourceString;
|
||||
},
|
||||
textblock(line, _1, rest) {
|
||||
return [line.ast, ...rest.ast].join('\n');
|
||||
},
|
||||
@@ -319,6 +342,9 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
_iter(...elements) {
|
||||
return elements.map((e) => e.ast);
|
||||
},
|
||||
tags(_1, list) {
|
||||
return { tags: list.ast };
|
||||
},
|
||||
meta(_1, dictionary) {
|
||||
let meta = mapPairListToKeyValPair(dictionary.ast);
|
||||
|
||||
|
||||
@@ -42,6 +42,14 @@ const jsonToBru = (json) => {
|
||||
bru += '}\n\n';
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
bru += 'tags [\n';
|
||||
for (const tag of tags) {
|
||||
bru += ` ${tag}\n`;
|
||||
}
|
||||
bru += ']\n\n';
|
||||
}
|
||||
|
||||
if (http && http.method) {
|
||||
bru += `${http.method} {
|
||||
url: ${http.url}`;
|
||||
|
||||
@@ -4,6 +4,11 @@ meta {
|
||||
seq: 1
|
||||
}
|
||||
|
||||
tags [
|
||||
foo
|
||||
bar
|
||||
]
|
||||
|
||||
get {
|
||||
url: https://api.textlocal.in/send/:id
|
||||
body: json
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"type": "http",
|
||||
"seq": "1"
|
||||
},
|
||||
"tags": ["foo", "bar"],
|
||||
"http": {
|
||||
"method": "get",
|
||||
"url": "https://api.textlocal.in/send/:id",
|
||||
|
||||
@@ -310,7 +310,8 @@ const requestSchema = Yup.object({
|
||||
.nullable(),
|
||||
assertions: Yup.array().of(keyValueSchema).nullable(),
|
||||
tests: Yup.string().nullable(),
|
||||
docs: Yup.string().nullable()
|
||||
docs: Yup.string().nullable(),
|
||||
tags: Yup.array().of(Yup.string().matches(/^[\w-]+$/, 'tag must be alphanumeric'))
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
@@ -9,6 +9,7 @@ describe('Request Schema Validation', () => {
|
||||
method: 'GET',
|
||||
headers: [],
|
||||
params: [],
|
||||
tags: ['smoke-test'],
|
||||
body: {
|
||||
mode: 'none'
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ const jsonToToml = (json) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (json.tags && json.tags.length) {
|
||||
formattedJson.tags = get(json, 'tags', []);
|
||||
}
|
||||
|
||||
if (json.headers && json.headers.length) {
|
||||
const hasDuplicateHeaders = keyValPairHasDuplicateKeys(json.headers);
|
||||
const hasReservedHeaders = keyValPairHasReservedKeys(json.headers);
|
||||
|
||||
@@ -24,6 +24,10 @@ const tomlToJson = (toml) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (json.tags && json.tags.length) {
|
||||
formattedJson.tags = get(json, 'tags', []);
|
||||
}
|
||||
|
||||
if (json.headers) {
|
||||
formattedJson.headers = [];
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"type": "http",
|
||||
"seq": 1
|
||||
},
|
||||
"tags": ["foo", "bar"],
|
||||
"http": {
|
||||
"method": "GET",
|
||||
"url": "https://reqres.in/api/users"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
tags = [ 'foo', 'bar' ]
|
||||
|
||||
[meta]
|
||||
name = 'Get users'
|
||||
type = 'http'
|
||||
|
||||
Reference in New Issue
Block a user