Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions internal/dashboard/assets/react/assets/index-DNopBGB_.css

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion internal/dashboard/assets/react/assets/index-RiEXN4Qh.css

This file was deleted.

4 changes: 2 additions & 2 deletions internal/dashboard/assets/react/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>devcloud Dashboard</title>
<script type="module" crossorigin src="/dashboard/assets/index-DQhVjlL6.js"></script>
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-RiEXN4Qh.css">
<script type="module" crossorigin src="/dashboard/assets/index-BVYd6qTL.js"></script>
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-DNopBGB_.css">
</head>
<body>
<div id="root"></div>
Expand Down
31 changes: 15 additions & 16 deletions web/dashboard/src/app/services/redis/RedisDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
listRedisKeys,
runRedisCommand,
} from './api'
import { highlightJSON, tryPrettyJSON } from './jsonHighlight'
import type { RedisCommandResponse, RedisKeyDetail, RedisKeySummary, RedisStatus } from './types'

const COMMAND_HISTORY_LIMIT = 8
Expand Down Expand Up @@ -504,9 +505,9 @@ function renderValueByType(detail: RedisKeyDetail): JSX.Element {
}

function StringValueView({ raw }: { raw: string }): JSX.Element {
const pretty = useMemo(() => tryFormatJSON(raw), [raw])
const pretty = useMemo(() => tryPrettyJSON(raw), [raw])
const [mode, setMode] = useState<'pretty' | 'raw'>(pretty ? 'pretty' : 'raw')
const display = mode === 'pretty' && pretty ? pretty : raw
const showPretty = mode === 'pretty' && pretty !== undefined
return (
<div className="redis-value-string">
<div className="redis-value-toolbar">
Expand Down Expand Up @@ -534,7 +535,9 @@ function StringValueView({ raw }: { raw: string }): JSX.Element {
</div>
) : null}
</div>
<pre className="redis-value-pre">{display}</pre>
<pre className="redis-value-pre json-code">
{showPretty ? highlightJSON(pretty!) : raw}
</pre>
</div>
)
}
Expand Down Expand Up @@ -567,7 +570,7 @@ function HashValueView({ entries }: { entries: string[] }): JSX.Element {
return (
<tr key={index}>
<th scope="row"><code>{field}</code></th>
<td><code>{value}</code></td>
<td><HashFieldValue raw={value} /></td>
</tr>
)
})}
Expand All @@ -576,6 +579,14 @@ function HashValueView({ entries }: { entries: string[] }): JSX.Element {
)
}

function HashFieldValue({ raw }: { raw: string }): JSX.Element {
const pretty = useMemo(() => tryPrettyJSON(raw), [raw])
if (pretty === undefined) {
return <code>{raw}</code>
}
return <pre className="redis-value-pre json-code redis-value-pre-inline">{highlightJSON(pretty)}</pre>
}

function SetValueView({ members }: { members: string[] }): JSX.Element {
return (
<ul className="redis-set-view" aria-label="Set members">
Expand Down Expand Up @@ -849,18 +860,6 @@ function parseCommand(value: string): { command: string; args: string[] } {
return { command, args }
}

function tryFormatJSON(value: string): string | undefined {
const trimmed = value.trim()
if (trimmed === '' || (trimmed[0] !== '{' && trimmed[0] !== '[' && trimmed[0] !== '"')) {
return undefined
}
try {
return JSON.stringify(JSON.parse(trimmed), null, 2)
} catch {
return undefined
}
}

function splitFirst(value: string, separator: string): [string, string] {
const index = value.indexOf(separator)
if (index === -1) {
Expand Down
68 changes: 68 additions & 0 deletions web/dashboard/src/app/services/redis/jsonHighlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { ReactNode } from 'react'

const TOKEN_PATTERN =
/("(?:\\.|[^"\\])*")(\s*:)?|(\b(?:true|false|null)\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g

export function highlightJSON(source: string): ReactNode[] {
const nodes: ReactNode[] = []
let lastIndex = 0
let keyCounter = 0

TOKEN_PATTERN.lastIndex = 0
let match: RegExpExecArray | null
while ((match = TOKEN_PATTERN.exec(source)) !== null) {
const [whole, stringLit, colonTail, keyword, number] = match
const start = match.index

if (start > lastIndex) {
nodes.push(source.slice(lastIndex, start))
}

if (stringLit !== undefined) {
const isKey = colonTail !== undefined
nodes.push(
<span className={isKey ? 'json-token-key' : 'json-token-string'} key={`t${keyCounter++}`}>
{stringLit}
</span>,
)
if (colonTail) {
nodes.push(colonTail)
}
} else if (keyword !== undefined) {
const cls = keyword === 'null' ? 'json-token-null' : 'json-token-bool'
nodes.push(
<span className={cls} key={`t${keyCounter++}`}>
{keyword}
</span>,
)
} else if (number !== undefined) {
nodes.push(
<span className="json-token-number" key={`t${keyCounter++}`}>
{number}
</span>,
)
} else {
nodes.push(whole)
}

lastIndex = start + whole.length
}

if (lastIndex < source.length) {
nodes.push(source.slice(lastIndex))
}

return nodes
}

export function tryPrettyJSON(value: string): string | undefined {
const trimmed = value.trim()
if (trimmed === '' || (trimmed[0] !== '{' && trimmed[0] !== '[' && trimmed[0] !== '"')) {
return undefined
}
try {
return JSON.stringify(JSON.parse(trimmed), null, 2)
} catch {
return undefined
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function RedshiftDashboard({ service }: RedshiftDashboardProps): JSX.Elem
}

return (
<div className="dynamodb-workspace">
<div className="redshift-workspace">
<Panel title="Clusters">
<div className="dynamodb-toolbar">
<span className="toolbar-count">{state.status === 'success' ? `${state.clusters.length} clusters` : 'Loading'}</span>
Expand Down
72 changes: 72 additions & 0 deletions web/dashboard/src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,17 @@ code {
align-items: start;
}

.redshift-workspace {
display: grid;
grid-template-columns: minmax(260px, 340px) minmax(320px, 1fr);
align-items: start;
gap: 12px;
}

.redshift-workspace > .panel:nth-of-type(3) {
grid-column: 1 / -1;
}

.service-card {
color: inherit;
text-decoration: none;
Expand Down Expand Up @@ -545,6 +556,8 @@ code {
border-bottom: 1px solid var(--border-default);
text-align: left;
vertical-align: top;
overflow-wrap: anywhere;
word-break: break-word;
}

.object-table th,
Expand Down Expand Up @@ -1135,6 +1148,7 @@ code {
.s3-workspace,
.gcs-workspace,
.dynamodb-workspace,
.redshift-workspace,
.mail-workspace {
grid-template-columns: 1fr;
}
Expand Down Expand Up @@ -2210,6 +2224,7 @@ textarea.s3-text-input {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
}

.redis-inspector-header {
Expand Down Expand Up @@ -2303,6 +2318,7 @@ textarea.s3-text-input {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}

.redis-section-head {
Expand Down Expand Up @@ -2403,6 +2419,7 @@ textarea.s3-text-input {
.redis-hash-table,
.redis-zset-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
font-size: 13px;
}
Expand All @@ -2416,10 +2433,65 @@ textarea.s3-text-input {
border-bottom: 1px solid var(--border-default);
vertical-align: top;
word-break: break-all;
overflow-wrap: anywhere;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12.5px;
}

.redis-hash-table th:first-child,
.redis-hash-table td:first-child {
width: 34%;
}

.redis-zset-table th:last-child,
.redis-zset-table td:last-child {
width: 96px;
}

.redis-hash-table code,
.redis-zset-table code {
display: inline-block;
max-width: 100%;
overflow: visible;
white-space: normal;
text-overflow: clip;
word-break: break-all;
overflow-wrap: anywhere;
}

.redis-value-pre-inline {
margin: 0;
max-height: 240px;
font-size: 12px;
line-height: 1.5;
}

.json-code {
color: var(--code-text);
}

.json-code .json-token-key {
color: #9ad1ff;
font-weight: 600;
}

.json-code .json-token-string {
color: #b6e7a8;
}

.json-code .json-token-number {
color: #f2c97d;
}

.json-code .json-token-bool {
color: #f4a8c2;
}

.json-code .json-token-null {
color: #b6bec8;
font-style: italic;
}

.redis-hash-table thead th,
.redis-zset-table thead th {
background: var(--surface-panel-muted);
Expand Down
Loading