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:
Baptiste Poulain 2024-05-22 15:23:12 +02:00 committed by GitHub
parent ff3ea33979
commit 91b5d0123e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 168 additions and 15 deletions

View File

@ -1,11 +1,110 @@
import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
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 dispatch = useDispatch();
@ -24,7 +123,6 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
handleSubmit(values.collectionLocation);
}
});
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
@ -52,7 +150,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
Name
</label>
<div className="mt-2">{collectionName}</div>
{translationLog && Object.keys(translationLog).length > 0 && (
<TranslationLog translationLog={translationLog} />
)}
<>
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
Location

View File

@ -14,14 +14,16 @@ import StyledWrapper from './StyledWrapper';
const TitleBar = () => {
const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
const dispatch = useDispatch();
const { ipcRenderer } = window;
const handleImportCollection = (collection) => {
const handleImportCollection = ({ collection, translationLog }) => {
setImportedCollection(collection);
setImportedTranslationLog(translationLog);
setImportCollectionModalOpen(false);
setImportCollectionLocationModalOpen(true);
};
@ -64,6 +66,7 @@ const TitleBar = () => {
{importCollectionLocationModalOpen ? (
<ImportCollectionLocation
collectionName={importedCollection.name}
translationLog={importedTranslationLog}
onClose={() => setImportCollectionLocationModalOpen(false)}
handleSubmit={handleImportCollectionLocation}
/>

View File

@ -13,6 +13,7 @@ import StyledWrapper from './StyledWrapper';
const Welcome = () => {
const dispatch = useDispatch();
const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
@ -23,8 +24,9 @@ const Welcome = () => {
);
};
const handleImportCollection = (collection) => {
const handleImportCollection = (collection, translationLog) => {
setImportedCollection(collection);
setImportedTranslationLog(translationLog);
setImportCollectionModalOpen(false);
setImportCollectionLocationModalOpen(true);
};
@ -44,6 +46,7 @@ const Welcome = () => {
) : null}
{importCollectionLocationModalOpen ? (
<ImportCollectionLocation
translationLog={importedTranslationLog}
collectionName={importedCollection.name}
onClose={() => setImportCollectionLocationModalOpen(false)}
handleSubmit={handleImportCollectionLocation}

View File

@ -54,6 +54,8 @@ const convertV21Auth = (array) => {
}, {});
};
const translationLog = {};
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
brunoParent.items = brunoParent.items || [];
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) {
i.event.forEach((event) => {
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)) {
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');
} else {
brunoRequestItem.request.script.req = options.enablePostmanTranslations.enabled
? postmanTranslation(event.script.exec[0])
? postmanTranslation(event.script.exec[0], () => pushTranslationLog('script', 0))
: `// ${event.script.exec[0]} `;
}
}
@ -137,11 +161,15 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
}
if (Array.isArray(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');
} else {
brunoRequestItem.request.tests = options.enablePostmanTranslations.enabled
? postmanTranslation(event.script.exec[0])
? postmanTranslation(event.script.exec[0], () => pushTranslationLog('test', 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) => {
return new Promise((resolve, reject) => {
fileDialog({ accept: 'application/json' })
@ -321,11 +364,12 @@ const importCollection = (options) => {
.then(transformItemsInCollection)
.then(hydrateSeqInCollection)
.then(validateSchema)
.then((collection) => resolve(collection))
.then((collection) => resolve({ collection, translationLog }))
.catch((err) => {
console.log(err);
reject(new BrunoError('Import collection failed'));
});
})
.then(() => logTranslationDetails(translationLog));
});
};

View File

@ -11,7 +11,9 @@ const replacements = {
'pm\\.response\\.to\\.have\\.status\\(': 'expect(res.getStatus()).to.equal(',
'pm\\.response\\.json\\(': 'res.getBody(',
'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]) => ({
@ -19,7 +21,7 @@ const compiledReplacements = Object.entries(replacements).map(([pattern, replace
replacement
}));
export const postmanTranslation = (script) => {
export const postmanTranslation = (script, logCallback) => {
try {
let modifiedScript = script;
let modified = false;
@ -29,8 +31,9 @@ export const postmanTranslation = (script) => {
modified = true;
}
}
if (modified && modifiedScript.includes('pm.')) {
if (modifiedScript.includes('pm.')) {
modifiedScript = modifiedScript.replace(/^(.*pm\..*)$/gm, '// $1');
logCallback?.();
}
return modifiedScript;
} catch (e) {