diff --git a/src/dashboard/DatabaseProfiler/DatabaseProfiler.react.js b/src/dashboard/DatabaseProfiler/DatabaseProfiler.react.js index 350e2b019..bbe96374a 100644 --- a/src/dashboard/DatabaseProfiler/DatabaseProfiler.react.js +++ b/src/dashboard/DatabaseProfiler/DatabaseProfiler.react.js @@ -113,16 +113,16 @@ class DatabaseProfile extends DashboardView { renderHeaders() { return [ - + Operation Type , Class , - + Execution Time (ms) , - + Executed At (UTC) ]; diff --git a/src/dashboard/DatabaseProfiler/DatabaseProfiler.scss b/src/dashboard/DatabaseProfiler/DatabaseProfiler.scss index e26ee17d0..ac3a8f745 100644 --- a/src/dashboard/DatabaseProfiler/DatabaseProfiler.scss +++ b/src/dashboard/DatabaseProfiler/DatabaseProfiler.scss @@ -2,13 +2,27 @@ z-index: 1000 !important; } +code[class*="language-"], pre[class*="language-"] { + background: #111214 !important; +} + +.pre-wrap { + max-height: auto; +} + .pageContainer { position: relative; height: 100vh; display: flex; flex-direction: column; - padding-top: 64px; /* Account for header */ - overflow: hidden; /* Prevent double scrollbars */ + padding-top: 64px; + /* Account for header */ + overflow: hidden; + /* Prevent double scrollbars */ +} + +.marginTopContent { + margin-top: 80px; } .mainContent { @@ -21,18 +35,19 @@ margin-left: 10px; cursor: pointer; color: #ffffff; - + &:hover { opacity: 0.8; } svg { - fill: currentColor; // Make SVG icon inherit the text color + fill: currentColor; // Make SVG icon inherit the text color } } .row { transition: all 0.3s ease; + &:hover { cursor: pointer; box-shadow: 0px 1px 0px 0px rgba(193, 226, 255, 0.16); @@ -43,7 +58,8 @@ .detailView { padding: 20px; background: #0c1e35; - margin-top: 0px; /* Account for toolbar height */ + margin-top: 0px; + /* Account for toolbar height */ box-sizing: border-box; } @@ -93,11 +109,15 @@ .detailSection { margin-bottom: 24px; + border-radius: 7px; + border: 0.1px solid #ffffff29; + padding: 20px 20px 10px 20px; + background-color: #10203a; h3 { color: #ffffff; - font-size: 14px; - margin-bottom: 12px; + font-size: 17px; + margin-bottom: 20px; font-weight: 600; } } @@ -105,28 +125,17 @@ .detailGrid { display: grid; grid-template-columns: 1fr; - gap: 16px; + gap: 5px; margin-bottom: 12px; - - &.twoColumns { - grid-template-columns: repeat(2, 1fr); - } - - @media (max-width: 1200px) { - &.twoColumns { - grid-template-columns: 1fr; - } - } } .detailRow { display: flex; + padding: 5px; align-items: center; - padding: 12px 16px; - background: rgba(255, 255, 255, 0.05); border-radius: 6px; + font-size: 15px; transition: all 0.2s ease; - min-height: 48px; box-sizing: border-box; &:hover { @@ -135,12 +144,13 @@ } .detailLabel { - color: rgba(255, 255, 255, 0.7); - font-weight: 500; - margin-right: 24px; - width: 160px; + color: #ffffff9c; + margin-right: 10px; flex-shrink: 0; - font-size: 13px; + + &::after { + content: ":"; + } } .detailValue { @@ -151,17 +161,18 @@ } .queryContainer { - background: rgba(255, 255, 255, 0.05); - padding: 12px; + margin-top: 20px; border-radius: 6px; font-size: 13px; width: 100%; line-height: 1.5; box-sizing: border-box; - max-height: 200px; + min-height: 200px; display: flex; flex-direction: column; - + margin-bottom: 20px; + position: relative; + h4 { margin: 0 0 8px 0; font-size: 13px; @@ -171,54 +182,40 @@ pre { width: 100%; flex: 1; - overflow: auto; margin: 0; - padding: 8px; - background: rgba(0, 0, 0, 0.2); + background: #111214 !important; + margin: 0 !important; border-radius: 4px; font-size: 12px; white-space: pre-wrap; - word-break: break-all; - max-height: 150px; - - &::-webkit-scrollbar { - width: 6px; - height: 6px; - } - - &::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); - border-radius: 3px; - } - - &::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.2); - border-radius: 3px; - - &:hover { - background: rgba(255, 255, 255, 0.3); - } - } + min-height: 200px; + border-radius: 4px; + padding: 0rem 1.5rem 1.5rem 0 !important; + overflow-x: auto; + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; + font-family: 'Roboto Mono', monospace !important; + padding-left: 3.5em !important; } } -.title { +.contentTitle { + background: #345070; color: #ffffff; font-size: 14px; font-weight: 500; - margin-bottom: 12px; -} + padding: 8px 30px; + border-radius: 5px 5px 0px 0px; -.code { - background: rgba(255, 255, 255, 0.02); - border-radius: 4px; - padding: 12px; + button { + float: right; + } } .booleanTag { display: inline-flex; align-items: center; - padding: 4px 8px; + padding: 4px 12px; border-radius: 4px; font-size: 12px; font-weight: 500; diff --git a/src/dashboard/DatabaseProfiler/DatabaseProfilerDetail.react.js b/src/dashboard/DatabaseProfiler/DatabaseProfilerDetail.react.js index f98ac6271..687cc85f8 100644 --- a/src/dashboard/DatabaseProfiler/DatabaseProfilerDetail.react.js +++ b/src/dashboard/DatabaseProfiler/DatabaseProfilerDetail.react.js @@ -5,9 +5,42 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ -import React from 'react'; + +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; +import ReactMarkdown from 'react-markdown'; import styles from './DatabaseProfiler.scss'; +import Prism from 'prismjs'; +import 'prismjs/plugins/line-numbers/prism-line-numbers'; +import 'prismjs/plugins/line-numbers/prism-line-numbers.css'; +import 'prismjs/components/prism-json'; +// eslint-disable-next-line no-unused-vars +import 'stylesheets/b4a-prisma.css'; + +const handleCopy = async (value) => { + try { + await navigator.clipboard.writeText(value.trim()); + } catch (err) { + console.error('Failed to copy text: ', err); + } +}; + +const CodeBlock = ({ language, value }) => { + useEffect(() => { + if (typeof Prism !== 'undefined') { + Prism.highlightAll(); + } + }, [value, language]); + + return ( +
+
{value.trim()}
+
+ ); +}; + + const DatabaseProfilerDetail = ({ data }) => { if (!data) { return
No data available
; @@ -27,6 +60,7 @@ const DatabaseProfilerDetail = ({ data }) => { responseLength = 0, hasSort = false, hasIndex = false, + executionStats, command = op || 'unknown', // Fallback to op if command is not present limit = 0, update, @@ -47,7 +81,7 @@ const DatabaseProfilerDetail = ({ data }) => { }; const renderDetailRow = (label, value) => ( -
+
{label} {value}
@@ -59,204 +93,123 @@ const DatabaseProfilerDetail = ({ data }) => { ); - const renderOperationSpecificDetails = () => { - if (!command || command === 'unknown') { - return ( -
-

Operation Details

-
-
{JSON.stringify(data, null, 2)}
+ const JsonCodeBlock = React.memo(({ title, data }) => { + const memoizedJson = React.useMemo(() => JSON.stringify(data, null, 2), [data]); + return ( +
+ {title && + <> +
+ {title} +
-
- ); - } + + } +
+ {`~~~json +${JSON.stringify(data, null, 2)} +~~~`} +
+
+ ); +}); + +JsonCodeBlock.propTypes = { + title: PropTypes.string, + data: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired +}; + +const DetailSection = React.memo(({ title, children }) => ( +
+

{title}

+
+ {children} +
+
+ )); + +DetailSection.propTypes = { + title: PropTypes.string.isRequired, + children: PropTypes.node.isRequired +}; + + const renderOperationSpecificDetails = () => { + let sectionTitle = 'Operation Details', subTitle = '', json, sortJson, updateJson; switch (command) { + case 'unknown': + subTitle = 'Command Details'; + json = data; + break; case 'aggregate': - return ( -
-

Aggregation Pipeline

-
-
{JSON.stringify(pipeline, null, 2)}
-
-
- ); - + subTitle = 'Pipeline'; + json = pipeline; + break; case 'count': - return ( -
-

Count Query

-
-
{JSON.stringify(query, null, 2)}
-
-
- ); - + subTitle = 'Query'; + json = query; + sortJson = sort; + break; case 'delete': - return ( -
-

Delete Operation

-
- {renderDetailRow('Documents Removed', nRemoved || 0)} -
-

Delete Query

-
{JSON.stringify(query, null, 2)}
-
-
-
- ); - + case 'remove': + subTitle = 'Query'; + json = query; + break; case 'distinct': - return ( -
-

Distinct Operation

-
- {renderDetailRow('Distinct Key', distinct?.key || '')} -
-

Distinct Query

-
{JSON.stringify(query, null, 2)}
-
-
-
- ); - + subTitle = 'Query'; + json = query; + break; case 'find': case 'query': - return ( -
-

Query Details

-
0 ? 'flex' : 'flow', gap: '20px' }}> -
-
Find Query
-
-
{JSON.stringify(query, null, 2)}
-
-
- - {Object.keys(sort).length > 0 && ( -
-
Sort Criteria
-
-
{JSON.stringify(sort, null, 2)}
-
-
- )} -
-
- ); - + subTitle = 'Query'; + json = query; + sortJson = sort; + break; case 'findAndModify': - return ( -
-

Find and Modify Operation

-
- {limit > 0 && renderDetailRow('Limit', limit)} -
-

Query Document

-
{JSON.stringify(query, null, 2)}
-
- {Object.keys(sort).length > 0 && ( -
-

Sort Criteria

-
{JSON.stringify(sort, null, 2)}
-
- )} - {update && ( -
-

Update Operations

-
{JSON.stringify(update, null, 2)}
-
- )} -
-
- ); - - case 'getMore': - return ( -
-

Get More Operation

-
- {renderDetailRow('Documents Returned', docsReturned)} -
-
- ); - - case 'insert': - return ( -
-

Insert Operation

-
- {renderDetailRow('Documents Inserted', nInserted || 0)} -
-
- ); - + subTitle = 'Query'; + json = query; + sortJson = sort; + updateJson = update; + break; case 'mapReduce': - return ( -
-

Map-Reduce Operation

-
- {mapReduce && ( - <> -
-

Map Function

-
{mapReduce.map}
-
-
-

Reduce Function

-
{mapReduce.reduce}
-
- {mapReduce.finalize && ( -
-

Finalize Function

-
{mapReduce.finalize}
-
- )} - - )} -
-
- ); - + subTitle = 'Map-Reduce Operation'; + json = { + map: mapReduce?.map, + reduce: mapReduce?.reduce, + finalize: mapReduce?.finalize + }; + break; case 'update': - return ( -
-

Update Operation

-
- {renderDetailRow('Documents Modified', nModified || 0)} -
-

Query

-
{JSON.stringify(query, null, 2)}
-
-
-

Update

-
{JSON.stringify(update, null, 2)}
-
-
-
- ); - - case 'remove': - return ( - <> -
-

Remove Operation

-
- {renderDetailRow('Documents Removed', nRemoved || 0)} - {renderDetailRow('Limit', limit || 0)} -
-
-
-

Query Details

-
-
{JSON.stringify(query, null, 2)}
-
-
- - ); + subTitle = 'Query'; + json = query; + sortJson = sort; + updateJson = update; + break; default: return null; } + + return ( + + {json && Object.keys(json).length > 0 && } + {sortJson && Object.keys(sortJson).length > 0 && } + {updateJson && Object.keys(updateJson).length > 0 && } + {distinct && Object.keys(distinct).length > 0 && } + + ); }; const shouldShowPerformanceMetrics = () => { @@ -265,21 +218,49 @@ const DatabaseProfilerDetail = ({ data }) => { return commandsWithMetrics.includes(command); }; - const renderOverview = () => ( -
-

Operation Overview

-
- {renderDetailRow('Command Type', command)} - {renderDetailRow('Class', className)} - {(command === 'find' || command === 'query') && renderDetailRow('Limit', limit || 0)} - {renderDetailRow('Total Execution Time', `${duration}ms`)} - {renderDetailRow('Last Execution Time', formatDate(ts))} + const getOperationDetails = () => { + switch (command) { + case 'find': + case 'query': + const queryTotal = executionStats?.nReturned || docsReturned; + return queryTotal !== undefined ? { title: 'Documents Returned', value: queryTotal } : null; + case 'count': + const countTotal = docsReturned || executionStats?.nReturned; + return countTotal !== undefined ? { title: 'Documents Counted', value: countTotal } : null; + case 'delete': + case 'remove': + return nRemoved ? { title: 'Documents Removed', value: nRemoved } : null; + case 'insert': + return nInserted ? { title: 'Documents Inserted', value: nInserted } : null; + case 'update': + return nModified ? { title: 'Documents Modified', value: nModified } : null; + case 'distinct': + return distinct?.key ? { title: 'Distinct Key', value: distinct.key } : null; + default: + return null; + } + }; + + const renderOverview = () => { + + return ( +
+

Operation Overview

+
+ {renderDetailRow('Command Type', command)} + {renderDetailRow('Class', className)} + {(command === 'find' || command === 'query') && renderDetailRow('Limit', limit || 0)} + {renderDetailRow('Duration', `${duration}ms`)} + {renderDetailRow('Timestamp', formatDate(ts))} +
-
- ); + ); + }; + + const operationDetails = getOperationDetails(); return ( -
+
{renderOverview()} {shouldShowPerformanceMetrics() && ( @@ -289,6 +270,7 @@ const DatabaseProfilerDetail = ({ data }) => { {renderDetailRow('Keys Examined', keysExamined)} {renderDetailRow('Docs Examined', docsExamined)} {renderDetailRow('Response Length', responseLength)} + {operationDetails && renderDetailRow(operationDetails.title, operationDetails.value)}
)}