mirror of
https://github.com/usebruno/bruno.git
synced 2025-06-21 20:41:41 +02:00
Feat/UI feedback : Visual + console feedback for failed postman translated imports (#2316)
* feat(translation-feedback): console log incomplete postman import translations with stats and details * feat(translation-feedback): warn instead of log, reformat layout * feat(translation-feedback): optional callback function, update index.spec.js * feat(ui-feedback): display translation errors in the UI before choosing import location * feat(ui-feedback): syntax fix --------- Co-authored-by: bpoulaindev <bpoulainpro@gmail.com>
This commit is contained in:
parent
ff3ea33979
commit
91b5d0123e
@ -1,11 +1,110 @@
|
|||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import Modal from 'components/Modal';
|
import Modal from 'components/Modal';
|
||||||
|
import { IconAlertTriangle, IconArrowRight, IconCaretDown, IconCaretRight, IconCopy } from '@tabler/icons';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => {
|
const TranslationLog = ({ translationLog }) => {
|
||||||
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
const preventSetShowDetails = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setShowDetails(!showDetails);
|
||||||
|
};
|
||||||
|
const copyClipboard = (e, value) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
navigator.clipboard.writeText(value);
|
||||||
|
toast.success('Copied to clipboard');
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col mt-2">
|
||||||
|
<div className="border-l-2 border-amber-500 dark:border-amber-300 bg-amber-50 dark:bg-amber-50/10 p-1.5 rounded-r">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<IconAlertTriangle className="h-4 w-4 text-amber-500 dark:text-amber-300" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-2">
|
||||||
|
<p className="text-xs text-amber-700 dark:text-amber-300">
|
||||||
|
<span className="font-semibold">Warning:</span> Some commands were not translated.{' '}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={(e) => preventSetShowDetails(e)}
|
||||||
|
className="flex w-fit items-center rounded px-2.5 py-1 mt-2 text-xs font-semibold ring-1 ring-inset bg-slate-50 dark:bg-slate-400/10 text-slate-700 dark:text-slate-300 ring-slate-600/10 dark:ring-slate-400/20"
|
||||||
|
>
|
||||||
|
See details
|
||||||
|
{showDetails ? <IconCaretDown size={16} className="ml-1" /> : <IconCaretRight size={16} className="ml-1" />}
|
||||||
|
</button>
|
||||||
|
{showDetails && (
|
||||||
|
<div className="flex relative flex-col text-xs max-w-[364px] max-h-[300px] overflow-scroll mt-2 p-2 bg-slate-50 dark:bg-slate-400/10 ring-1 ring-inset rounded text-slate-700 dark:text-slate-300 ring-slate-600/20 dark:ring-slate-400/20">
|
||||||
|
<span className="font-semibold flex items-center">
|
||||||
|
Impacted Collections: {Object.keys(translationLog || {}).length}
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold flex items-center">
|
||||||
|
Impacted Lines:{' '}
|
||||||
|
{Object.values(translationLog || {}).reduce(
|
||||||
|
(acc, curr) => acc + (curr.script?.length || 0) + (curr.test?.length || 0),
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="my-1">
|
||||||
|
The numbers after 'script' and 'test' indicate the line numbers of incomplete translations.
|
||||||
|
</span>
|
||||||
|
<ul>
|
||||||
|
{Object.entries(translationLog || {}).map(([name, value]) => (
|
||||||
|
<li key={name} className="list-none text-xs font-semibold">
|
||||||
|
<div className="font-semibold flex items-center text-xs whitespace-nowrap">
|
||||||
|
<IconCaretRight className="min-w-4 max-w-4 -ml-1" />
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{value.script && (
|
||||||
|
<div className="flex items-center text-xs font-light mb-1">
|
||||||
|
<span className="mr-2">
|
||||||
|
test :
|
||||||
|
{value.script.map((scriptValue, index) => (
|
||||||
|
<span className="flex items-center" key={`script_${name}_${index}`}>
|
||||||
|
<span className="text-xs font-light">{scriptValue}</span>
|
||||||
|
{index < value.script.length - 1 && <> - </>}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{value.test && (
|
||||||
|
<div className="flex items-center text-xs font-light mb-1">
|
||||||
|
<span className="mr-2">test :</span>
|
||||||
|
{value.test.map((testValue, index) => (
|
||||||
|
<div className="flex items-center" key={`test_${name}_${index}`}>
|
||||||
|
<span className="text-xs font-light">{testValue}</span>
|
||||||
|
{index < value.test.length - 1 && <> - </>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
className="absolute top-1 right-1 flex w-fit items-center rounded p-2 text-xs font-semibold ring-1 ring-inset bg-slate-50 dark:bg-slate-400/10 text-slate-700 dark:text-slate-300 ring-slate-600/10 dark:ring-slate-400/20"
|
||||||
|
onClick={(e) => copyClipboard(e, JSON.stringify(translationLog))}
|
||||||
|
>
|
||||||
|
<IconCopy size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, translationLog }) => {
|
||||||
const inputRef = useRef();
|
const inputRef = useRef();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@ -24,7 +123,6 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
|||||||
handleSubmit(values.collectionLocation);
|
handleSubmit(values.collectionLocation);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const browse = () => {
|
const browse = () => {
|
||||||
dispatch(browseDirectory())
|
dispatch(browseDirectory())
|
||||||
.then((dirPath) => {
|
.then((dirPath) => {
|
||||||
@ -52,7 +150,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
|||||||
Name
|
Name
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-2">{collectionName}</div>
|
<div className="mt-2">{collectionName}</div>
|
||||||
|
{translationLog && Object.keys(translationLog).length > 0 && (
|
||||||
|
<TranslationLog translationLog={translationLog} />
|
||||||
|
)}
|
||||||
<>
|
<>
|
||||||
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
|
||||||
Location
|
Location
|
||||||
|
@ -14,14 +14,16 @@ import StyledWrapper from './StyledWrapper';
|
|||||||
|
|
||||||
const TitleBar = () => {
|
const TitleBar = () => {
|
||||||
const [importedCollection, setImportedCollection] = useState(null);
|
const [importedCollection, setImportedCollection] = useState(null);
|
||||||
|
const [importedTranslationLog, setImportedTranslationLog] = useState({});
|
||||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { ipcRenderer } = window;
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
const handleImportCollection = (collection) => {
|
const handleImportCollection = ({ collection, translationLog }) => {
|
||||||
setImportedCollection(collection);
|
setImportedCollection(collection);
|
||||||
|
setImportedTranslationLog(translationLog);
|
||||||
setImportCollectionModalOpen(false);
|
setImportCollectionModalOpen(false);
|
||||||
setImportCollectionLocationModalOpen(true);
|
setImportCollectionLocationModalOpen(true);
|
||||||
};
|
};
|
||||||
@ -64,6 +66,7 @@ const TitleBar = () => {
|
|||||||
{importCollectionLocationModalOpen ? (
|
{importCollectionLocationModalOpen ? (
|
||||||
<ImportCollectionLocation
|
<ImportCollectionLocation
|
||||||
collectionName={importedCollection.name}
|
collectionName={importedCollection.name}
|
||||||
|
translationLog={importedTranslationLog}
|
||||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||||
handleSubmit={handleImportCollectionLocation}
|
handleSubmit={handleImportCollectionLocation}
|
||||||
/>
|
/>
|
||||||
|
@ -13,6 +13,7 @@ import StyledWrapper from './StyledWrapper';
|
|||||||
const Welcome = () => {
|
const Welcome = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [importedCollection, setImportedCollection] = useState(null);
|
const [importedCollection, setImportedCollection] = useState(null);
|
||||||
|
const [importedTranslationLog, setImportedTranslationLog] = useState({});
|
||||||
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
|
||||||
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
|
||||||
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
|
||||||
@ -23,8 +24,9 @@ const Welcome = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImportCollection = (collection) => {
|
const handleImportCollection = (collection, translationLog) => {
|
||||||
setImportedCollection(collection);
|
setImportedCollection(collection);
|
||||||
|
setImportedTranslationLog(translationLog);
|
||||||
setImportCollectionModalOpen(false);
|
setImportCollectionModalOpen(false);
|
||||||
setImportCollectionLocationModalOpen(true);
|
setImportCollectionLocationModalOpen(true);
|
||||||
};
|
};
|
||||||
@ -44,6 +46,7 @@ const Welcome = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
{importCollectionLocationModalOpen ? (
|
{importCollectionLocationModalOpen ? (
|
||||||
<ImportCollectionLocation
|
<ImportCollectionLocation
|
||||||
|
translationLog={importedTranslationLog}
|
||||||
collectionName={importedCollection.name}
|
collectionName={importedCollection.name}
|
||||||
onClose={() => setImportCollectionLocationModalOpen(false)}
|
onClose={() => setImportCollectionLocationModalOpen(false)}
|
||||||
handleSubmit={handleImportCollectionLocation}
|
handleSubmit={handleImportCollectionLocation}
|
||||||
|
@ -54,6 +54,8 @@ const convertV21Auth = (array) => {
|
|||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const translationLog = {};
|
||||||
|
|
||||||
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
|
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
|
||||||
brunoParent.items = brunoParent.items || [];
|
brunoParent.items = brunoParent.items || [];
|
||||||
const folderMap = {};
|
const folderMap = {};
|
||||||
@ -114,7 +116,25 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
/* struct of translation log
|
||||||
|
{
|
||||||
|
[collectionName]: {
|
||||||
|
script: [index1, index2],
|
||||||
|
test: [index1, index2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// type could be script or test
|
||||||
|
const pushTranslationLog = (type, index) => {
|
||||||
|
if (!translationLog[i.name]) {
|
||||||
|
translationLog[i.name] = {};
|
||||||
|
}
|
||||||
|
if (!translationLog[i.name][type]) {
|
||||||
|
translationLog[i.name][type] = [];
|
||||||
|
}
|
||||||
|
translationLog[i.name][type].push(index + 1);
|
||||||
|
};
|
||||||
if (i.event) {
|
if (i.event) {
|
||||||
i.event.forEach((event) => {
|
i.event.forEach((event) => {
|
||||||
if (event.listen === 'prerequest' && event.script && event.script.exec) {
|
if (event.listen === 'prerequest' && event.script && event.script.exec) {
|
||||||
@ -123,11 +143,15 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
|||||||
}
|
}
|
||||||
if (Array.isArray(event.script.exec)) {
|
if (Array.isArray(event.script.exec)) {
|
||||||
brunoRequestItem.request.script.req = event.script.exec
|
brunoRequestItem.request.script.req = event.script.exec
|
||||||
.map((line) => (options.enablePostmanTranslations.enabled ? postmanTranslation(line) : `// ${line}`))
|
.map((line, index) =>
|
||||||
|
options.enablePostmanTranslations.enabled
|
||||||
|
? postmanTranslation(line, () => pushTranslationLog('script', index))
|
||||||
|
: `// ${line}`
|
||||||
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
} else {
|
} else {
|
||||||
brunoRequestItem.request.script.req = options.enablePostmanTranslations.enabled
|
brunoRequestItem.request.script.req = options.enablePostmanTranslations.enabled
|
||||||
? postmanTranslation(event.script.exec[0])
|
? postmanTranslation(event.script.exec[0], () => pushTranslationLog('script', 0))
|
||||||
: `// ${event.script.exec[0]} `;
|
: `// ${event.script.exec[0]} `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -137,11 +161,15 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
|||||||
}
|
}
|
||||||
if (Array.isArray(event.script.exec)) {
|
if (Array.isArray(event.script.exec)) {
|
||||||
brunoRequestItem.request.tests = event.script.exec
|
brunoRequestItem.request.tests = event.script.exec
|
||||||
.map((line) => (options.enablePostmanTranslations.enabled ? postmanTranslation(line) : `// ${line}`))
|
.map((line, index) =>
|
||||||
|
options.enablePostmanTranslations.enabled
|
||||||
|
? postmanTranslation(line, () => pushTranslationLog('test', index))
|
||||||
|
: `// ${line}`
|
||||||
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
} else {
|
} else {
|
||||||
brunoRequestItem.request.tests = options.enablePostmanTranslations.enabled
|
brunoRequestItem.request.tests = options.enablePostmanTranslations.enabled
|
||||||
? postmanTranslation(event.script.exec[0])
|
? postmanTranslation(event.script.exec[0], () => pushTranslationLog('test', 0))
|
||||||
: `// ${event.script.exec[0]} `;
|
: `// ${event.script.exec[0]} `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -313,6 +341,21 @@ const parsePostmanCollection = (str, options) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logTranslationDetails = (translationLog) => {
|
||||||
|
if (Object.keys(translationLog || {}).length > 0) {
|
||||||
|
console.warn(
|
||||||
|
`[Postman Translation Logs]
|
||||||
|
Collections incomplete : ${Object.keys(translationLog || {}).length}` +
|
||||||
|
`\nTotal lines incomplete : ${Object.values(translationLog || {}).reduce(
|
||||||
|
(acc, curr) => acc + (curr.script?.length || 0) + (curr.test?.length || 0),
|
||||||
|
0
|
||||||
|
)}` +
|
||||||
|
`\nSee details below :`,
|
||||||
|
translationLog
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const importCollection = (options) => {
|
const importCollection = (options) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fileDialog({ accept: 'application/json' })
|
fileDialog({ accept: 'application/json' })
|
||||||
@ -321,11 +364,12 @@ const importCollection = (options) => {
|
|||||||
.then(transformItemsInCollection)
|
.then(transformItemsInCollection)
|
||||||
.then(hydrateSeqInCollection)
|
.then(hydrateSeqInCollection)
|
||||||
.then(validateSchema)
|
.then(validateSchema)
|
||||||
.then((collection) => resolve(collection))
|
.then((collection) => resolve({ collection, translationLog }))
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
reject(new BrunoError('Import collection failed'));
|
reject(new BrunoError('Import collection failed'));
|
||||||
});
|
})
|
||||||
|
.then(() => logTranslationDetails(translationLog));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -11,7 +11,9 @@ const replacements = {
|
|||||||
'pm\\.response\\.to\\.have\\.status\\(': 'expect(res.getStatus()).to.equal(',
|
'pm\\.response\\.to\\.have\\.status\\(': 'expect(res.getStatus()).to.equal(',
|
||||||
'pm\\.response\\.json\\(': 'res.getBody(',
|
'pm\\.response\\.json\\(': 'res.getBody(',
|
||||||
'pm\\.expect\\(': 'expect(',
|
'pm\\.expect\\(': 'expect(',
|
||||||
'pm\\.environment\\.has\\(([^)]+)\\)': 'bru.getEnvVar($1) !== undefined && bru.getEnvVar($1) !== null'
|
'pm\\.environment\\.has\\(([^)]+)\\)': 'bru.getEnvVar($1) !== undefined && bru.getEnvVar($1) !== null',
|
||||||
|
'pm\\.response\\.code': 'res.getStatus()',
|
||||||
|
'pm\\.response\\.text\\(': 'res.getBody()?.toString('
|
||||||
};
|
};
|
||||||
|
|
||||||
const compiledReplacements = Object.entries(replacements).map(([pattern, replacement]) => ({
|
const compiledReplacements = Object.entries(replacements).map(([pattern, replacement]) => ({
|
||||||
@ -19,7 +21,7 @@ const compiledReplacements = Object.entries(replacements).map(([pattern, replace
|
|||||||
replacement
|
replacement
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const postmanTranslation = (script) => {
|
export const postmanTranslation = (script, logCallback) => {
|
||||||
try {
|
try {
|
||||||
let modifiedScript = script;
|
let modifiedScript = script;
|
||||||
let modified = false;
|
let modified = false;
|
||||||
@ -29,8 +31,9 @@ export const postmanTranslation = (script) => {
|
|||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (modified && modifiedScript.includes('pm.')) {
|
if (modifiedScript.includes('pm.')) {
|
||||||
modifiedScript = modifiedScript.replace(/^(.*pm\..*)$/gm, '// $1');
|
modifiedScript = modifiedScript.replace(/^(.*pm\..*)$/gm, '// $1');
|
||||||
|
logCallback?.();
|
||||||
}
|
}
|
||||||
return modifiedScript;
|
return modifiedScript;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user