diff --git a/dist/icons/16.svg b/dist/icons/16.svg new file mode 100644 index 0000000..0f00fc6 --- /dev/null +++ b/dist/icons/16.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dist/manifest.json b/dist/manifest.json index cdb04aa..d3aba20 100644 --- a/dist/manifest.json +++ b/dist/manifest.json @@ -1,6 +1,6 @@ { "name": "Net logs", - "version": "1.0.0", + "version": "1.0.1", "manifest_version": 3, "minimum_chrome_version": "88", "description": "Extendable network logs debugger", diff --git a/package.json b/package.json index b3861c8..bf65f0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netlogs", - "version": "1.0.0", + "version": "1.0.1", "description": "Web extension for custom network logs representation", "main": "index.js", "author": "artboomy", @@ -40,6 +40,7 @@ "react-inspector": "https://github.com/Artboomy/react-inspector/releases/download/5.3.5/react-inspector-5.3.5.tgz", "react-jss": "10.5.0", "react-multi-select-component": "^4.3.4", + "react-toastify": "9.1.3", "react-use": "17.2.4", "zustand": "3.3.3" }, diff --git a/src/api/runtime.ts b/src/api/runtime.ts index 30b0672..875617a 100644 --- a/src/api/runtime.ts +++ b/src/api/runtime.ts @@ -5,7 +5,7 @@ class SandboxRuntime { return { manifest_version: 3, name: 'Net logs', - version: '1.0.0' + version: '1.0.1' }; } diff --git a/src/app/devtools.ts b/src/app/devtools.ts index 6fb765f..cff2f8c 100644 --- a/src/app/devtools.ts +++ b/src/app/devtools.ts @@ -1 +1,2 @@ -chrome.devtools.panels.create('📜 Net logs', 'icons/16.png', 'panel.html'); +const title = navigator.userAgent.includes('Edg') ? 'Net logs' : '📜 Net logs'; +chrome.devtools.panels.create(title, 'icons/16', 'panel.html'); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 5e8c74b..c25388b 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -5,10 +5,11 @@ import { useListStore } from '../controllers/network'; import runtime from '../api/runtime'; import { theme } from '../theme/light'; import { Har } from 'har-format'; -import { callParentVoid, isExtension } from '../utils'; +import { callParent, isExtension } from '../utils'; import { IconButton, ICONS } from './IconButton'; import { useHotkey } from '../hooks/useHotkey'; import { MimetypeSelect } from './MimetypeSelect'; +import { toast } from 'react-toastify'; const useStyles = createUseStyles({ root: { @@ -65,12 +66,19 @@ const doExport = () => { comment: 'Format: http://www.softwareishard.com/blog/har-12-spec/' } }; - callParentVoid( - 'download', - JSON.stringify({ - fileName: getFileName(), - data: JSON.stringify(fileData) - }) + toast.promise( + callParent( + 'download', + JSON.stringify({ + fileName: getFileName(), + data: JSON.stringify(fileData) + }) + ), + { + pending: 'Exporting...', + success: 'Exported', + error: 'Error exporting' + } ); }; diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 0451947..51f5021 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,12 +1,19 @@ import React, { FC } from 'react'; +import { callParentVoid } from '../utils'; type LinkProps = { text: string; href: string; }; export const Link: FC = ({ text, href }) => { + const handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + callParentVoid('chrome.tabs.create', href); + return false; + }; + return ( - + {text} ); diff --git a/src/components/PanelMain.tsx b/src/components/PanelMain.tsx index 26066b2..c5d111f 100644 --- a/src/components/PanelMain.tsx +++ b/src/components/PanelMain.tsx @@ -13,6 +13,8 @@ import useDebounce from 'react-use/lib/useDebounce'; import { Footer } from './Footer'; import { FilterContext } from '../context/FilterContext'; import { useHotkey } from '../hooks/useHotkey'; +import { ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.min.css'; const useStyles = createUseStyles({ '@global': { @@ -108,6 +110,7 @@ export const PanelMain: React.FC = () => { /> + diff --git a/src/components/Section.tsx b/src/components/Section.tsx index 3c3932b..ed9a3e1 100644 --- a/src/components/Section.tsx +++ b/src/components/Section.tsx @@ -1,6 +1,7 @@ import React, { FC, ReactNode } from 'react'; import { createUseStyles } from 'react-jss'; import { theme } from '../theme/light'; +import cn from 'classnames'; type TItem = { name: string; @@ -22,9 +23,17 @@ const useStyles = createUseStyles({ key: { fontWeight: 'bold', color: theme.section.key + }, + valueNumber: { + color: 'rgb(28, 0, 207)' + }, + valueString: { + color: 'rgb(196, 26, 22)' } }); +const isQuoted = (s: unknown) => + typeof s === 'string' && s.startsWith('"') && s.endsWith('"'); export const Section: FC = ({ title, items }) => { const styles = useStyles(); return ( @@ -34,7 +43,16 @@ export const Section: FC = ({ title, items }) => { {items.map(({ name, value }, index) => (
- {name}: {value} + {name}:{' '} + + {value} +
))} diff --git a/src/components/list/DropContainer.tsx b/src/components/list/DropContainer.tsx index d3a02d3..5a3563d 100644 --- a/src/components/list/DropContainer.tsx +++ b/src/components/list/DropContainer.tsx @@ -10,6 +10,7 @@ import { createUseStyles } from 'react-jss'; import ContentOnlyItem from '../../models/ContentOnlyItem'; import { ItemType } from '../../models/types'; import TransactionItem from '../../models/TransactionItem'; +import { toast } from 'react-toastify'; const useStyles = createUseStyles({ dropZone: { @@ -29,52 +30,55 @@ export const DropContainer: FC = ({ children }) => { const [{ canDrop, isOver }, dropRef] = useDrop( () => ({ accept: [NativeTypes.FILE], - drop(item: { files: File[] }) { + async drop(item: { files: File[] }) { const file = item.files[0]; - if (isFileSupported(file.name)) { - parseFile(file).then( - (log) => { - if (log?.log?.entries) { - try { - setList( - [ - new ContentOnlyItem({ - timestamp: new Date().getTime(), - tag: 'NET LOGS', - content: `Opened file "${file.name}"` - }), - ...log.log.entries.map( - (request) => { - let ItemContstructor; - switch (request.comment) { - case ItemType.ContentOnly: - ItemContstructor = ContentOnlyItem; - break; - case ItemType.Transaction: - ItemContstructor = TransactionItem; - break; - default: - ItemContstructor = NetworkItem; - } - return ItemContstructor.fromJSON( - request - ); - } - ) - ], - false - ); - } catch (e) { - window.alert('Invalid har file'); + if (!isFileSupported(file.name)) { + toast.error('Only json files are supported'); + } + let log: Har | null = null; + const toastId = toast('Loading file...'); + try { + log = await parseFile(file); + toast.dismiss(toastId); + } catch (e) { + toast.dismiss(toastId); + toast.error('Error parsing file'); + } + if (!log) { + return; + } + if (!log?.log?.entries) { + toast.error('Invalid har file'); + return; + } + try { + setList( + [ + new ContentOnlyItem({ + timestamp: new Date().getTime(), + tag: 'NET LOGS', + content: `Opened file "${file.name}"` + }), + ...log.log.entries.map((request) => { + let ItemConstructor; + switch (request.comment) { + case ItemType.ContentOnly: + ItemConstructor = ContentOnlyItem; + break; + case ItemType.Transaction: + ItemConstructor = TransactionItem; + break; + default: + ItemConstructor = NetworkItem; } - } else { - window.alert('Invalid har file'); - } - }, - (e) => window.alert(`Error parsing file ${e.message}`) + return ItemConstructor.fromJSON(request); + }) + ], + false ); - } else { - window.alert('Only json files are supported'); + } catch (e) { + console.log('Error occurred:', e); + toast.error('Invalid har file'); } }, collect: (monitor) => ({ diff --git a/src/controllers/network.ts b/src/controllers/network.ts index c55f1c3..c87bc8c 100644 --- a/src/controllers/network.ts +++ b/src/controllers/network.ts @@ -19,14 +19,28 @@ type TStore = { setList: (newList: ItemList, isDynamic?: boolean) => void; }; -export const useListStore = create((set) => ({ +export const useListStore = create((set, get) => ({ list: [], isDynamic: true, isPreserve: false, mimeTypes: new Set(), clear: () => set({ list: [], isDynamic: true, mimeTypes: new Set() }), - setList: (newList: ItemList, isDynamic = true) => - set({ list: newList, isDynamic }) + setList: (newList: ItemList, isDynamic = true) => { + const newState = { + list: newList, + isDynamic, + mimeTypes: new Set([...get().mimeTypes]) + }; + if (!isDynamic) { + newList.forEach((request) => { + const mimeType = request.toJSON().response.content.mimeType; + if (mimeType && !newState.mimeTypes.has(mimeType)) { + newState.mimeTypes.add(mimeType); + } + }); + } + set(newState); + } })); class Network { diff --git a/src/controllers/settings/profiles/default.ts b/src/controllers/settings/profiles/default.ts index 4763f30..9961bf2 100644 --- a/src/controllers/settings/profiles/default.ts +++ b/src/controllers/settings/profiles/default.ts @@ -20,7 +20,10 @@ export const defaultProfile: IProfile = { let params; const postData = request.request.postData; const method = request.request.method; - if ((method === 'POST' || method === 'PATCH') && postData) { + if ( + (method === 'POST' || method === 'PATCH' || method === 'PUT') && + postData + ) { if (postData.text) { try { params = JSON.parse(postData.text); diff --git a/src/sandboxUtils.ts b/src/sandboxUtils.ts index f838768..a3ed6fc 100644 --- a/src/sandboxUtils.ts +++ b/src/sandboxUtils.ts @@ -117,7 +117,14 @@ export async function wrapSandbox(): Promise { window.chrome?.devtools.inspectedWindow.reload({}); break; case 'download': - downloadAsZip(data); + downloadAsZip(data).finally(() => { + postSandbox({ + id, + type, + data: '' + }); + resolve(); + }); break; default: console.warn(`Unrecognized type ${type}`); @@ -127,18 +134,20 @@ export async function wrapSandbox(): Promise { }); } -function downloadAsZip(dataString: string): void { +function downloadAsZip(dataString: string): Promise { const { fileName, data } = JSON.parse(dataString); // const blob = new Blob([data], { type: 'application/json' }); const zip = new JSZip(); zip.file(`${fileName}.har`, data); - zip.generateAsync({ - type: 'blob', - compression: 'DEFLATE', - compressionOptions: { - level: 9 - } - }).then((content) => { - download(`${fileName}.netlogs.zip`, content); - }); + return zip + .generateAsync({ + type: 'blob', + compression: 'DEFLATE', + compressionOptions: { + level: 9 + } + }) + .then((content) => { + download(`${fileName}.netlogs.zip`, content); + }); } diff --git a/yarn.lock b/yarn.lock index beaa683..b3663b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8247,7 +8247,7 @@ number-is-nan@^1.0.0: object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-copy@^0.1.0: version "0.1.0" @@ -9379,6 +9379,13 @@ react-textarea-autosize@^8.3.0: use-composed-ref "^1.0.0" use-latest "^1.0.0" +react-toastify@9.1.3: + version "9.1.3" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.3.tgz#1e798d260d606f50e0fab5ee31daaae1d628c5ff" + integrity sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg== + dependencies: + clsx "^1.1.1" + react-universal-interface@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b"