From 259eee184b64b6dafc1e66bf9adfc26265e373f8 Mon Sep 17 00:00:00 2001 From: Antti Sonkeri Date: Sat, 20 Jul 2024 11:41:56 +0300 Subject: [PATCH 1/9] Add support for defining tags in Bru files Add a generic `list` block to Bru grammar that holds simple text values in square brackets separated by newlines. Example usage: ``` meta { name: Login type: http seq: 1 } tags [ smoke-test authentication ] post { url: https://example.com/login body: json } ``` --- packages/bruno-lang/v2/src/bruToJson.js | 30 +++++++++++++++++-- packages/bruno-lang/v2/src/jsonToBru.js | 10 ++++++- .../bruno-lang/v2/tests/fixtures/request.bru | 5 ++++ .../bruno-lang/v2/tests/fixtures/request.json | 1 + 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index e99b690c2..107d93a4b 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -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,10 +19,17 @@ 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 { - BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)* + BruFile = (meta | tags | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)* auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body bodyforms = bodyformurlencoded | bodymultipart | bodyfile @@ -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 http = get | post | put | delete | patch | options | head | connect | trace @@ -297,6 +311,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'); }, @@ -318,6 +341,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); diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index c7395e2ff..dcef501b4 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -30,7 +30,7 @@ const getValueString = (value) => { }; const jsonToBru = (json) => { - const { meta, http, params, headers, auth, body, script, tests, vars, assertions, docs } = json; + const { meta, tags, http, params, headers, auth, body, script, tests, vars, assertions, docs } = json; let bru = ''; @@ -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}`; diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index c3f81b780..5230a8132 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -4,6 +4,11 @@ meta { seq: 1 } +tags [ + foo + bar +] + get { url: https://api.textlocal.in/send/:id body: json diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index 5cdebec00..b97419241 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -4,6 +4,7 @@ "type": "http", "seq": "1" }, + "tags": ["foo", "bar"], "http": { "method": "get", "url": "https://api.textlocal.in/send/:id", From 540cc234d701475d9cb2a1eb7394a76038d198ff Mon Sep 17 00:00:00 2001 From: Antti Sonkeri Date: Wed, 24 Jul 2024 00:10:22 +0300 Subject: [PATCH 2/9] Filter requests by tags in bruno-cli A request is only included in the test run if its tags include at least one of the include tags and none of the exclude tags specified in the CLI options. --- packages/bruno-cli/src/commands/run.js | 22 +++++++++- packages/bruno-cli/src/utils/bru.js | 1 + packages/bruno-common/src/index.ts | 3 +- packages/bruno-common/src/tags/index.spec.ts | 43 ++++++++++++++++++++ packages/bruno-common/src/tags/index.ts | 13 ++++++ 5 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 packages/bruno-common/src/tags/index.spec.ts create mode 100644 packages/bruno-common/src/tags/index.ts diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index ccbfa8581..ca3e315dd 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -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') @@ -268,7 +277,9 @@ const handler = async function (argv) { reporterSkipHeaders, clientCertConfig, noproxy, - delay + delay, + tags: includeTags, + excludeTags } = argv; const collectionPath = process.cwd(); @@ -353,7 +364,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 +400,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 +458,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 }); diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js index 07844a455..d78c153e3 100644 --- a/packages/bruno-cli/src/utils/bru.js +++ b/packages/bruno-cli/src/utils/bru.js @@ -60,6 +60,7 @@ const bruToJson = (bru) => { type: requestType, name: _.get(json, 'meta.name'), seq: !isNaN(sequence) ? Number(sequence) : 1, + tags: _.get(json, 'tags', []), request: { method: _.upperCase(_.get(json, 'http.method')), url: _.get(json, 'http.url'), diff --git a/packages/bruno-common/src/index.ts b/packages/bruno-common/src/index.ts index c49e79d58..81f0cae72 100644 --- a/packages/bruno-common/src/index.ts +++ b/packages/bruno-common/src/index.ts @@ -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'; diff --git a/packages/bruno-common/src/tags/index.spec.ts b/packages/bruno-common/src/tags/index.spec.ts new file mode 100644 index 000000000..46d307502 --- /dev/null +++ b/packages/bruno-common/src/tags/index.spec.ts @@ -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); + }); +}); diff --git a/packages/bruno-common/src/tags/index.ts b/packages/bruno-common/src/tags/index.ts new file mode 100644 index 000000000..a2adfe798 --- /dev/null +++ b/packages/bruno-common/src/tags/index.ts @@ -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; From 570be81467d307d69753d6a8b75bc3f0f5e22db8 Mon Sep 17 00:00:00 2001 From: Antti Sonkeri Date: Sat, 27 Jul 2024 11:45:57 +0300 Subject: [PATCH 3/9] Tag support in jsonToToml and tomlToJson --- packages/bruno-toml/src/jsonToToml.js | 4 ++++ packages/bruno-toml/src/tomlToJson.js | 4 ++++ packages/bruno-toml/tests/methods/get/request.json | 1 + packages/bruno-toml/tests/methods/get/request.toml | 2 ++ 4 files changed, 11 insertions(+) diff --git a/packages/bruno-toml/src/jsonToToml.js b/packages/bruno-toml/src/jsonToToml.js index c61191ad8..492417530 100644 --- a/packages/bruno-toml/src/jsonToToml.js +++ b/packages/bruno-toml/src/jsonToToml.js @@ -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); diff --git a/packages/bruno-toml/src/tomlToJson.js b/packages/bruno-toml/src/tomlToJson.js index 37b50ad39..faa70468e 100644 --- a/packages/bruno-toml/src/tomlToJson.js +++ b/packages/bruno-toml/src/tomlToJson.js @@ -24,6 +24,10 @@ const tomlToJson = (toml) => { } }; + if (json.tags && json.tags.length) { + formattedJson.tags = get(json, 'tags', []); + } + if (json.headers) { formattedJson.headers = []; diff --git a/packages/bruno-toml/tests/methods/get/request.json b/packages/bruno-toml/tests/methods/get/request.json index 2fb3955f1..4f37293d2 100644 --- a/packages/bruno-toml/tests/methods/get/request.json +++ b/packages/bruno-toml/tests/methods/get/request.json @@ -4,6 +4,7 @@ "type": "http", "seq": 1 }, + "tags": ["foo", "bar"], "http": { "method": "GET", "url": "https://reqres.in/api/users" diff --git a/packages/bruno-toml/tests/methods/get/request.toml b/packages/bruno-toml/tests/methods/get/request.toml index ae34e0771..66e75af35 100644 --- a/packages/bruno-toml/tests/methods/get/request.toml +++ b/packages/bruno-toml/tests/methods/get/request.toml @@ -1,3 +1,5 @@ +tags = [ 'foo', 'bar' ] + [meta] name = 'Get users' type = 'http' From 508c7018c67d29e35c7ca93a1c37dda54b876ee2 Mon Sep 17 00:00:00 2001 From: Antti Sonkeri Date: Sat, 27 Jul 2024 22:55:59 +0300 Subject: [PATCH 4/9] Add view for adding and deleting request tags --- .../RequestPane/HttpRequestPane/index.js | 7 ++ .../RequestPane/Tags/TagList/StyledWrapper.js | 25 +++++++ .../RequestPane/Tags/TagList/TagList.js | 69 +++++++++++++++++++ .../src/components/RequestPane/Tags/index.js | 39 +++++++++++ .../ReduxStore/slices/collections/index.js | 40 ++++++++++- .../bruno-app/src/utils/collections/index.js | 3 +- packages/bruno-electron/src/bru/index.js | 9 ++- .../bruno-schema/src/collections/index.js | 3 +- .../src/collections/requestSchema.spec.js | 1 + 9 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 packages/bruno-app/src/components/RequestPane/Tags/TagList/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/Tags/TagList/TagList.js create mode 100644 packages/bruno-app/src/components/RequestPane/Tags/index.js diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js index 126613504..3bbe682e6 100644 --- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js @@ -17,6 +17,7 @@ import { find, get } from 'lodash'; import Documentation from 'components/Documentation/index'; import HeightBoundContainer from 'ui/HeightBoundContainer'; import { useEffect } from 'react'; +import Tags from 'components/RequestPane/Tags/index'; const ContentIndicator = () => { return ( @@ -77,6 +78,9 @@ const HttpRequestPane = ({ item, collection }) => { case 'docs': { return ; } + case 'tags': { + return ; + } default: { return
404 | Not found
; } @@ -170,6 +174,9 @@ const HttpRequestPane = ({ item, collection }) => { Docs {docs && docs.length > 0 && } +
selectTab('tags')}> + Tags +
{focusedTab.requestPaneTab === 'body' ? (
diff --git a/packages/bruno-app/src/components/RequestPane/Tags/TagList/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Tags/TagList/StyledWrapper.js new file mode 100644 index 000000000..651fc761b --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Tags/TagList/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/RequestPane/Tags/TagList/TagList.js b/packages/bruno-app/src/components/RequestPane/Tags/TagList/TagList.js new file mode 100644 index 000000000..7248d54da --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Tags/TagList/TagList.js @@ -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 ( + +
    + {tags && tags.length + ? tags.map((_tag) => ( +
  • + {_tag} + +
  • + )) + : null} +
+ {isEditing ? ( + + ) : ( + + )} +
+ ); +}; + +export default TagList; diff --git a/packages/bruno-app/src/components/RequestPane/Tags/index.js b/packages/bruno-app/src/components/RequestPane/Tags/index.js new file mode 100644 index 000000000..ab6763aae --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Tags/index.js @@ -0,0 +1,39 @@ +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 ; +}; + +export default Tags; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index df1fc63bc..db29f7bc0 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -2235,9 +2235,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 { @@ -2350,6 +2384,8 @@ export const { collectionGetOauth2CredentialsByUrl, updateFolderAuth, updateFolderAuthMode, + addRequestTag, + deleteRequestTag } = collectionsSlice.actions; export default collectionsSlice.reducer; diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 5b0d28026..0509d4d38 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -542,7 +542,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 } }; diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js index 946d95519..9fdaf36c2 100644 --- a/packages/bruno-electron/src/bru/index.js +++ b/packages/bruno-electron/src/bru/index.js @@ -141,7 +141,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', []) } }; @@ -206,7 +207,8 @@ const jsonToBru = async (json) => { }, assertions: _.get(json, 'request.assertions', []), tests: _.get(json, 'request.tests', ''), - docs: _.get(json, 'request.docs', '') + docs: _.get(json, 'request.docs', ''), + tags: _.get(json, 'request.tags', []) }; const bru = jsonToBruV2(bruJson); @@ -247,7 +249,8 @@ const jsonToBruViaWorker = async (json) => { }, assertions: _.get(json, 'request.assertions', []), tests: _.get(json, 'request.tests', ''), - docs: _.get(json, 'request.docs', '') + docs: _.get(json, 'request.docs', ''), + tags: _.get(json, 'request.tags', []) }; const bru = await bruParserWorker?.jsonToBru(bruJson) diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index af4b13434..092f637a2 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -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(); diff --git a/packages/bruno-schema/src/collections/requestSchema.spec.js b/packages/bruno-schema/src/collections/requestSchema.spec.js index 9fd223cb2..3e17e9190 100644 --- a/packages/bruno-schema/src/collections/requestSchema.spec.js +++ b/packages/bruno-schema/src/collections/requestSchema.spec.js @@ -9,6 +9,7 @@ describe('Request Schema Validation', () => { method: 'GET', headers: [], params: [], + tags: ['smoke-test'], body: { mode: 'none' } From 44daba72cd5110d2d0696929554397fa1fcb01c7 Mon Sep 17 00:00:00 2001 From: Antti Sonkeri Date: Sun, 28 Jul 2024 14:28:03 +0300 Subject: [PATCH 5/9] Add tag filtering to collection runner --- .../src/components/RunnerResults/index.jsx | 46 ++++++++++++++++++- .../ReduxStore/slices/collections/actions.js | 5 +- .../bruno-electron/src/ipc/network/index.js | 12 ++++- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx index cbf099e5b..d8749c425 100644 --- a/packages/bruno-app/src/components/RunnerResults/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/index.jsx @@ -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)} />
+
+
+ + setTagsEnabled(!tagsEnabled)} + /> +
+ {tagsEnabled && ( +
+
+ Included tags: + setTags({ ...tags, include: [...tags.include, tag] })} + onTagRemove={(tag) => setTags({ ...tags, include: tags.include.filter((t) => t !== tag) })} + /> +
+
+ Excluded tags: + setTags({ ...tags, exclude: [...tags.exclude, tag] })} + onTagRemove={(tag) => setTags({ ...tags, exclude: tags.exclude.filter((t) => t !== tag) })} + /> +
+
+ )} +