diff --git a/ui/package-lock.json b/ui/package-lock.json index a0fa2a952..97b475cc3 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -7747,9 +7747,9 @@ "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" }, "highlight.js": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.2.tgz", - "integrity": "sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg==" + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.3.1.tgz", + "integrity": "sha512-PUhCRnPjLtiLHZAQ5A/Dt5F8cWZeMyj9KRsACsWT+OD6OP0x6dp5OmT5jdx0JgEyPxPZZIPQpRN2TciUT7occw==" }, "hmac-drbg": { "version": "1.0.1", @@ -10234,6 +10234,11 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, + "json-beautify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/json-beautify/-/json-beautify-1.1.1.tgz", + "integrity": "sha512-17j+Hk2lado0xqKtUcyAjK0AtoHnPSIgktWRsEXgdFQFG9UnaGw6CHa0J7xsvulxRpFl6CrkDFHght1p5ZJc4A==" + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -10612,6 +10617,13 @@ "requires": { "fault": "^1.0.0", "highlight.js": "~10.7.0" + }, + "dependencies": { + "highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" + } } }, "lru-cache": { @@ -13577,6 +13589,34 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-lowlight": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-lowlight/-/react-lowlight-3.0.0.tgz", + "integrity": "sha512-s0+T81PsCbUZYd/0XrplGc6kQEUdiwLKI0G6umJP1ViqRoZRCvSuHvXOy20Usd2ywDKWLuVETQgBDPeNQhPNZg==", + "requires": { + "lowlight": "^2.4.1" + }, + "dependencies": { + "fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "requires": { + "format": "^0.2.0" + } + }, + "lowlight": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-2.4.1.tgz", + "integrity": "sha512-mQkAG0zGQ9lcYecEft+hl9uV1fD6HpURA83/TYrsxKvb8xX2mfyB+aaV/A/aWmhhEcWVzr9Cc+l/fvUYfEUumw==", + "requires": { + "@types/hast": "^2.0.0", + "fault": "^2.0.0", + "highlight.js": "~11.3.0" + } + } + } + }, "react-refresh": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz", @@ -13663,6 +13703,13 @@ "lowlight": "^1.17.0", "prismjs": "^1.22.0", "refractor": "^3.2.0" + }, + "dependencies": { + "highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" + } } }, "react-toastify": { @@ -18149,11 +18196,24 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" }, + "xml-formatter": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-2.6.0.tgz", + "integrity": "sha512-+bQeoiE5W3CJdDCHTlveYSWFfQWnYB3uHGeRJ6LlEsL5kT++mWy9iN1cMeEDfBbgOnXO2DNUbmQ6elkR/mCcjg==", + "requires": { + "xml-parser-xo": "^3.2.0" + } + }, "xml-name-validator": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, + "xml-parser-xo": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-3.2.0.tgz", + "integrity": "sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg==" + }, "xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/ui/package.json b/ui/package.json index f8fcebd2a..71b1ed470 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,6 +15,8 @@ "@types/react-dom": "^17.0.3", "@uiw/react-textarea-code-editor": "^1.4.12", "axios": "^0.21.1", + "highlight.js": "^11.3.1", + "json-beautify": "^1.1.1", "jsonpath": "^1.1.1", "moment": "^2.29.1", "node-sass": "^5.0.0", @@ -23,12 +25,14 @@ "react": "^17.0.2", "react-copy-to-clipboard": "^5.0.3", "react-dom": "^17.0.2", + "react-lowlight": "^3.0.0", "react-scripts": "4.0.3", "react-scrollable-feed-virtualized": "^1.4.9", "react-syntax-highlighter": "^15.4.3", "react-toastify": "^8.0.3", "typescript": "^4.2.4", - "web-vitals": "^1.1.1" + "web-vitals": "^1.1.1", + "xml-formatter": "^2.6.0" }, "scripts": { "start": "react-scripts start", diff --git a/ui/src/components/EntryDetailed/EntrySections.tsx b/ui/src/components/EntryDetailed/EntrySections.tsx index 17bc33357..9a777705f 100644 --- a/ui/src/components/EntryDetailed/EntrySections.tsx +++ b/ui/src/components/EntryDetailed/EntrySections.tsx @@ -6,6 +6,8 @@ import FancyTextDisplay from "../UI/FancyTextDisplay"; import Queryable from "../UI/Queryable"; import Checkbox from "../UI/Checkbox"; import ProtobufDecoder from "protobuf-decoder"; +import {default as jsonBeautify} from "json-beautify"; +import {default as xmlBeautify} from "xml-formatter"; interface EntryViewLineProps { label: string; @@ -121,23 +123,41 @@ export const EntryBodySection: React.FC = ({ contentType, selector, }) => { - const MAXIMUM_BYTES_TO_HIGHLIGHT = 10000; // The maximum of chars to highlight in body, in case the response can be megabytes - const supportedLanguages = [['html', 'html'], ['json', 'json'], ['application/grpc', 'json']]; // [[indicator, languageToUse],...] - const jsonLikeFormats = ['json']; + const MAXIMUM_BYTES_TO_FORMAT = 1000000; // The maximum of chars to highlight in body, in case the response can be megabytes + const jsonLikeFormats = ['json', 'yaml', 'yml']; + const xmlLikeFormats = ['xml', 'html']; const protobufFormats = ['application/grpc']; - const [isWrapped, setIsWrapped] = useState(false); + const supportedFormats = jsonLikeFormats.concat(xmlLikeFormats, protobufFormats); - const formatTextBody = (body): string => { - const chunk = body.slice(0, MAXIMUM_BYTES_TO_HIGHLIGHT); - const bodyBuf = encoding === 'base64' ? atob(chunk) : chunk; + const [isPretty, setIsPretty] = useState(true); + const [showLineNumbers, setShowLineNumbers] = useState(true); + const [decodeBase64, setDecodeBase64] = useState(true); + + const isBase64Encoding = encoding === 'base64'; + const supportsPrettying = supportedFormats.some(format => contentType?.indexOf(format) > -1); + + const formatTextBody = (body: any): string => { + if (!decodeBase64) return body; + + const chunk = body.slice(0, MAXIMUM_BYTES_TO_FORMAT); + const bodyBuf = isBase64Encoding ? atob(chunk) : chunk; + + if (!isPretty) return bodyBuf; try { if (jsonLikeFormats.some(format => contentType?.indexOf(format) > -1)) { - return JSON.stringify(JSON.parse(bodyBuf), null, 2); + return jsonBeautify(JSON.parse(bodyBuf), null, 2, 80); + } else if (xmlLikeFormats.some(format => contentType?.indexOf(format) > -1)) { + return xmlBeautify(bodyBuf, { + indentation: ' ', + filter: (node) => node.type !== 'Comment', + collapseContent: true, + lineSeparator: '\n' + }); } else if (protobufFormats.some(format => contentType?.indexOf(format) > -1)) { // Replace all non printable characters (ASCII) const protobufDecoder = new ProtobufDecoder(bodyBuf, true); - return JSON.stringify(protobufDecoder.decode().toSimple(), null, 2); + return jsonBeautify(protobufDecoder.decode().toSimple(), null, 2, 80); } } catch (error) { console.error(error); @@ -145,13 +165,6 @@ export const EntryBodySection: React.FC = ({ return bodyBuf; } - const getLanguage = (mimetype) => { - const chunk = content?.slice(0, 100); - if (chunk.indexOf('html') > 0 || chunk.indexOf('HTML') > 0) return supportedLanguages[0][1]; - const language = supportedLanguages.find(el => (mimetype + contentType).indexOf(el[0]) > -1); - return language ? language[1] : 'default'; - } - return {content && content?.length > 0 && = ({ query={`${selector} == r".*"`} updateQuery={updateQuery} > - - - - {encoding && } - -
+
+ {supportsPrettying &&
+ {setIsPretty(!isPretty)}}/> +
} + {supportsPrettying && Pretty} -
setIsWrapped(!isWrapped)}> -
- {}}/> +
+ {setShowLineNumbers(!showLineNumbers)}}/>
- Wrap text + Line numbers + + {isBase64Encoding &&
+ {setDecodeBase64(!decodeBase64)}}/> +
} + {isBase64Encoding && Decode Base64}
} @@ -334,7 +349,6 @@ export const EntryContractSection: React.FC = ({color } {contractContent && diff --git a/ui/src/components/Filters.tsx b/ui/src/components/Filters.tsx index 0afc7325d..9e281c9b6 100644 --- a/ui/src/components/Filters.tsx +++ b/ui/src/components/Filters.tsx @@ -167,7 +167,6 @@ export const QueryForm: React.FC = ({query, setQuery, background This is a simple query that matches to HTTP packets with request path "/catalogue": = ({query, setQuery, background The same query can be negated for HTTP path and written like this: = ({query, setQuery, background The syntax supports regular expressions. Here is a query that matches the HTTP requests that send JSON to a server: = ({query, setQuery, background Here is another query that matches HTTP responses with status code 4xx: = ({query, setQuery, background The same exact query can be as integer comparison: = 400`} language="python" @@ -212,7 +207,6 @@ export const QueryForm: React.FC = ({query, setQuery, background The results can be queried based on their timestamps: = ({query, setQuery, background Since Mizu supports various protocols like gRPC, AMQP, Kafka and Redis. It's possible to write complex queries that match multiple protocols like this: = ({query, setQuery, background Such that; clicking this icon in left-pane, would append the query below: = ({query, setQuery, background A query that compares one selector to another is also a valid query: = ({query, setQuery, background true if the given selector's value starts with the string: = ({query, setQuery, background true if the given selector's value ends with the string: = ({query, setQuery, background true if the given selector's value contains the string: = ({query, setQuery, background returns the UNIX timestamp which is the equivalent of the time that's provided by the string. Invalid input evaluates to false: = datetime("10/19/2021, 6:29:02.593 PM")`} language="python" @@ -312,7 +299,6 @@ export const QueryForm: React.FC = ({query, setQuery, background limits the number of records that are streamed back as a result of a query. Always evaluates to true: code[class*=\"language-\"]": { - "background": "#F7F9FC", - "padding": ".1em", - "borderRadius": ".3em" - }, - "comment": { - "color": "#5d6aa0" - }, - "prolog": { - "color": "#494677" - }, - "doctype": { - "color": "#494677" - }, - "cdata": { - "color": "#494677" - }, - "punctuation": { - "color": "#494677" - }, - ".namespace": { - "Opacity": ".7" - }, - "property": { - "color": "#627ef7" - }, - "keyword": { - "color": "#627ef7" - }, - "tag": { - "color": "#627ef7" - }, - "class-name": { - "color": "#3eb545", - "textDecoration": "underline" - }, - "boolean": { - "color": "#3eb545" - }, - "constant": { - "color": "#3eb545" - }, - "symbol": { - "color": "#ff3a30" - }, - "deleted": { - "color": "#ff3a30" - }, - "number": { - "color": "#ff16f7" - }, - "selector": { - "color": "rgb(9,224,19)" - }, - "attr-name": { - "color": "rgb(9,224,19)" - }, - "string": { - "color": "rgb(9,224,19)" - }, - "char": { - "color": "rgb(9,224,19)" - }, - "builtin": { - "color": "rgb(9,224,19)" - }, - "inserted": { - "color": "rgb(9,224,19)" - }, - "variable": { - "color": "#C6C5FE" - }, - "operator": { - "color": "#A1A1A1" - }, - "entity": { - "color": "#fdab2b", - "cursor": "help" - }, - "url": { - "color": "#96CBFE" - }, - ".language-css .token.string": { - "color": "#87C38A" - }, - ".style .token.string": { - "color": "#87C38A" - }, - "atrule": { - "color": "#fdab2b" - }, - "attr-value": { - "color": "#f8c575" - }, - "function": { - "color": "#fdab2b" - }, - "regex": { - "color": "#fab248" - }, - "important": { - "color": "#fd971f", - "fontWeight": "bold" - }, - "bold": { - "fontWeight": "bold" - }, - "italic": { - "fontStyle": "italic" - } -}; diff --git a/ui/src/components/UI/SyntaxHighlighter/index.scss b/ui/src/components/UI/SyntaxHighlighter/index.scss index 623c71509..d735e6fe6 100644 --- a/ui/src/components/UI/SyntaxHighlighter/index.scss +++ b/ui/src/components/UI/SyntaxHighlighter/index.scss @@ -26,12 +26,24 @@ } } -.wrapped{ - pre { - code { - &:last-child { - white-space: pre-wrap!important - } - } - } -} \ No newline at end of file +code.hljs { + white-space: pre-wrap; +} + +code.hljs:before { + counter-reset: listing; +} + +code.hljs .hljs-marker-line { + counter-increment: listing; +} + +code.hljs .hljs-marker-line:before { + content: counter(listing) " "; + display: inline-block; + width: 3rem; + padding-left: auto; + margin-left: auto; + text-align: right; + opacity: .5; +} diff --git a/ui/src/components/UI/SyntaxHighlighter/index.tsx b/ui/src/components/UI/SyntaxHighlighter/index.tsx index 3b2efe644..313971bbf 100644 --- a/ui/src/components/UI/SyntaxHighlighter/index.tsx +++ b/ui/src/components/UI/SyntaxHighlighter/index.tsx @@ -1,30 +1,47 @@ import React from 'react'; -import {Prism as SyntaxHighlighterContainer} from 'react-syntax-highlighter'; -import {highlighterStyle} from './highlighterStyle' +import Lowlight from 'react-lowlight' +import 'highlight.js/styles/atom-one-light.css' import './index.scss'; +import xml from 'highlight.js/lib/languages/xml' +import json from 'highlight.js/lib/languages/json' +import protobuf from 'highlight.js/lib/languages/protobuf' +import javascript from 'highlight.js/lib/languages/javascript' +import actionscript from 'highlight.js/lib/languages/actionscript' +import wasm from 'highlight.js/lib/languages/wasm' +import handlebars from 'highlight.js/lib/languages/handlebars' +import yaml from 'highlight.js/lib/languages/yaml' +import python from 'highlight.js/lib/languages/python' + +Lowlight.registerLanguage('python', python); +Lowlight.registerLanguage('xml', xml); +Lowlight.registerLanguage('json', json); +Lowlight.registerLanguage('yaml', yaml); +Lowlight.registerLanguage('protobuf', protobuf); +Lowlight.registerLanguage('javascript', javascript); +Lowlight.registerLanguage('actionscript', actionscript); +Lowlight.registerLanguage('wasm', wasm); +Lowlight.registerLanguage('handlebars', handlebars); + interface Props { code: string; - style?: any; showLineNumbers?: boolean; - className?: string; language?: string; - isWrapped?: boolean; } export const SyntaxHighlighter: React.FC = ({ - code, - style = highlighterStyle, - showLineNumbers = true, - className, - language = 'python', - isWrapped = false, - }) => { - return
- - {code ?? ""} - -
; + code, + showLineNumbers = false, + language = null + }) => { + const markers = showLineNumbers ? code.split("\n").map((item, i) => { + return { + line: i + 1, + className: 'hljs-marker-line' + } + }) : []; + + return
; }; export default SyntaxHighlighter;