@dbx-tools/ui 0.9.112 → 0.9.117
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export const PACKAGE_IDENTIFIER = "@dbx-tools/ui";
|
|
6
|
-
export const PACKAGE_VERSION = "0.9.
|
|
6
|
+
export const PACKAGE_VERSION = "0.9.117";
|
|
7
7
|
export * as authReactAuthClient from "./src/auth/react/auth-client.ts";
|
|
8
8
|
export * as authReactAuthGate from "./src/auth/react/auth-gate.tsx";
|
|
9
9
|
export * as authReactPasskeyManager from "./src/auth/react/passkey-manager.tsx";
|
|
@@ -38,7 +38,8 @@ export type { EmailComposeProps } from "./src/email/react/email-compose.tsx";
|
|
|
38
38
|
export { joinAddresses, parseAddresses, attachmentNames } from "./src/email/react/fields.ts";
|
|
39
39
|
export type { EmailDraft } from "./src/email/react/fields.ts";
|
|
40
40
|
export type { BrandPreset, BrandPickerProps } from "./src/react/brand-picker.tsx";
|
|
41
|
-
export { JsonBlock } from "./src/react/highlighted-code.tsx";
|
|
41
|
+
export { HighlightedCodeBlock, JsonBlock } from "./src/react/highlighted-code.tsx";
|
|
42
|
+
export type { DetectedCodeLanguage } from "./src/react/highlighted-code.tsx";
|
|
42
43
|
export type { RecordPreviewProps } from "./src/react/record-preview.tsx";
|
|
43
44
|
export type { RecordPreviewRow } from "./src/react/record-preview-data.ts";
|
|
44
45
|
export type { SearchBoxProps } from "./src/search/react/search-box.tsx";
|
package/package.json
CHANGED
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@databricks/appkit-ui": "0.81.0",
|
|
33
|
-
"@dbx-tools/shared-auth": "^0.9.
|
|
34
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
35
|
-
"@dbx-tools/shared-email": "^0.9.
|
|
36
|
-
"@dbx-tools/shared-email-template": "^0.9.
|
|
37
|
-
"@dbx-tools/shared-search": "^0.9.
|
|
33
|
+
"@dbx-tools/shared-auth": "^0.9.117",
|
|
34
|
+
"@dbx-tools/shared-core": "^0.9.117",
|
|
35
|
+
"@dbx-tools/shared-email": "^0.9.117",
|
|
36
|
+
"@dbx-tools/shared-email-template": "^0.9.117",
|
|
37
|
+
"@dbx-tools/shared-search": "^0.9.117",
|
|
38
38
|
"lucide-react": "^0.554.0",
|
|
39
39
|
"shiki": "^3.0.0",
|
|
40
40
|
"streamdown": "^2.5.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"publishConfig": {
|
|
45
45
|
"access": "public"
|
|
46
46
|
},
|
|
47
|
-
"version": "0.9.
|
|
47
|
+
"version": "0.9.117",
|
|
48
48
|
"dbxToolsConfig": {
|
|
49
49
|
"tags": [
|
|
50
50
|
"ui"
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"exports": {
|
|
55
55
|
"./react": "./src/react/index.ts",
|
|
56
56
|
"./styles.css": "./src/styles.css",
|
|
57
|
+
"./*": "./src/*/index.ts",
|
|
57
58
|
"./branding/react": "./src/branding/react/index.ts",
|
|
58
59
|
"./branding/browser": "./src/branding/browser.ts",
|
|
59
60
|
"./branding/assets": "./src/branding/generated/assets.ts",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// GENERATED by projen watch - DO NOT EDIT.
|
|
2
|
+
// Regenerated from the exporting modules in ./src.
|
|
3
|
+
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
|
+
|
|
5
|
+
export * as assets from "./assets.ts";
|
|
6
|
+
export { dbxToolsAssetSvg, dbxToolsAssetDataUrls } from "./assets.ts";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// GENERATED by projen watch - DO NOT EDIT.
|
|
2
|
+
// Regenerated from the exporting modules in ./src.
|
|
3
|
+
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
|
+
|
|
5
|
+
export * as browser from "./browser.ts";
|
|
6
|
+
export * as generated from "./generated/index.ts";
|
|
7
|
+
export { resolveBrandAsset } from "./browser.ts";
|
|
8
|
+
export type { BrandAssetResolver, ApplyBrandContextOptions } from "./browser.ts";
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { json } from "@dbx-tools/shared-core";
|
|
7
8
|
import { useEffect, useState } from "react";
|
|
8
9
|
|
|
9
10
|
import { cn } from "./appkit-ui.ts";
|
|
@@ -11,6 +12,30 @@ import { highlightToHtml } from "./shiki-plugin.ts";
|
|
|
11
12
|
|
|
12
13
|
export { createShikiPlugin, highlightToHtml } from "./shiki-plugin.ts";
|
|
13
14
|
|
|
15
|
+
/** Code languages detected automatically in free-form text fields. */
|
|
16
|
+
export type DetectedCodeLanguage = "sql" | "python" | "typescript" | "bash" | "json";
|
|
17
|
+
|
|
18
|
+
/** Detect a supported code language from a complete free-form text value. */
|
|
19
|
+
export function detectCodeLanguage(source: string): DetectedCodeLanguage | undefined {
|
|
20
|
+
const trimmed = source.trim();
|
|
21
|
+
if (/^(?:select|with|insert|update|delete|merge|create|alter)\b/i.test(trimmed)) return "sql";
|
|
22
|
+
if (/^(?:from\s+\S+\s+import|import\s+\S+|def\s+\w+|class\s+\w+):?/m.test(trimmed)) {
|
|
23
|
+
return "python";
|
|
24
|
+
}
|
|
25
|
+
if (/^(?:const|let|var|interface|type|export|import)\b/m.test(trimmed)) return "typescript";
|
|
26
|
+
if (/^(?:#!.*\b(?:ba|z|k)?sh\b|(?:cd|ls|cat|grep|find|bun|npm|pnpm|yarn)\s)/m.test(trimmed)) {
|
|
27
|
+
return "bash";
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
(trimmed.startsWith("{") || trimmed.startsWith("[")) &&
|
|
31
|
+
trimmed.length > 1 &&
|
|
32
|
+
json.parse(trimmed) !== undefined
|
|
33
|
+
) {
|
|
34
|
+
return "json";
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
/** Resolve syntax-highlighted HTML while preserving plaintext during lazy load. */
|
|
15
40
|
export function useHighlightedHtml(source: string, language: string): string | null {
|
|
16
41
|
const [highlighted, setHighlighted] = useState<{
|
|
@@ -32,21 +57,35 @@ export function useHighlightedHtml(source: string, language: string): string | n
|
|
|
32
57
|
: null;
|
|
33
58
|
}
|
|
34
59
|
|
|
35
|
-
/** Render
|
|
36
|
-
export const
|
|
37
|
-
|
|
60
|
+
/** Render code with the shared lazily loaded Shiki runtime. */
|
|
61
|
+
export const HighlightedCodeBlock = ({
|
|
62
|
+
source,
|
|
63
|
+
language,
|
|
64
|
+
className,
|
|
65
|
+
}: {
|
|
66
|
+
source: string;
|
|
67
|
+
language: string;
|
|
68
|
+
className?: string;
|
|
69
|
+
}) => {
|
|
70
|
+
const html = useHighlightedHtml(source, language);
|
|
38
71
|
return (
|
|
39
72
|
<pre
|
|
73
|
+
data-language={language}
|
|
40
74
|
className={cn(
|
|
41
75
|
"max-w-full overflow-auto whitespace-pre-wrap break-words font-mono text-xs leading-relaxed",
|
|
42
76
|
className,
|
|
43
77
|
)}
|
|
44
78
|
>
|
|
45
79
|
{html === null ? (
|
|
46
|
-
<code>{
|
|
80
|
+
<code>{source}</code>
|
|
47
81
|
) : (
|
|
48
82
|
<code data-dbx-highlighted-code dangerouslySetInnerHTML={{ __html: html }} />
|
|
49
83
|
)}
|
|
50
84
|
</pre>
|
|
51
85
|
);
|
|
52
86
|
};
|
|
87
|
+
|
|
88
|
+
/** Render formatted JSON with the shared lazily loaded Shiki runtime. */
|
|
89
|
+
export const JsonBlock = ({ json, className }: { json: string; className?: string }) => (
|
|
90
|
+
<HighlightedCodeBlock source={json} language="json" className={className} />
|
|
91
|
+
);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { stringUtils } from "@dbx-tools/shared-core";
|
|
7
|
+
import { json, object as objectUtils, stringUtils } from "@dbx-tools/shared-core";
|
|
8
8
|
|
|
9
9
|
/** One field shown in the table view of {@link RecordPreview}. */
|
|
10
10
|
export interface RecordPreviewRow {
|
|
@@ -39,6 +39,18 @@ export function formatRecordPreviewJson(value: unknown): string {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Parse a string whose complete value is a serialized JSON object or array. */
|
|
43
|
+
export function parseNestedJsonText(
|
|
44
|
+
value: unknown,
|
|
45
|
+
): Record<string, unknown> | unknown[] | undefined {
|
|
46
|
+
if (typeof value !== "string") return undefined;
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return undefined;
|
|
49
|
+
const parsed = json.parse(trimmed);
|
|
50
|
+
if (Array.isArray(parsed)) return parsed;
|
|
51
|
+
return objectUtils.isRecord(parsed) ? parsed : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
/** YAML document fence at the start of a markdown file (`---` ... `---`). */
|
|
43
55
|
const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
44
56
|
|
|
@@ -21,11 +21,12 @@ import {
|
|
|
21
21
|
TooltipTrigger,
|
|
22
22
|
cn,
|
|
23
23
|
} from "./appkit-ui.ts";
|
|
24
|
-
import { JsonBlock } from "./highlighted-code.tsx";
|
|
24
|
+
import { detectCodeLanguage, HighlightedCodeBlock, JsonBlock } from "./highlighted-code.tsx";
|
|
25
25
|
import {
|
|
26
26
|
formatRecordPreviewJson,
|
|
27
27
|
looksLikeMarkdown,
|
|
28
28
|
markdownForPreview,
|
|
29
|
+
parseNestedJsonText,
|
|
29
30
|
recordPreviewRows,
|
|
30
31
|
type RecordPreviewRow,
|
|
31
32
|
} from "./record-preview-data.ts";
|
|
@@ -35,6 +36,7 @@ export {
|
|
|
35
36
|
formatRecordPreviewJson,
|
|
36
37
|
looksLikeMarkdown,
|
|
37
38
|
markdownForPreview,
|
|
39
|
+
parseNestedJsonText,
|
|
38
40
|
recordPreviewRows,
|
|
39
41
|
} from "./record-preview-data.ts";
|
|
40
42
|
|
|
@@ -42,7 +44,8 @@ export {
|
|
|
42
44
|
const MARKDOWN_CELL_CLASSES =
|
|
43
45
|
"min-w-0 max-w-full text-[11px] leading-snug [&_p]:my-0.5 [&_p]:leading-snug [&_ul]:my-0.5 [&_ol]:my-0.5 [&_li]:my-0 [&_h1]:my-1 [&_h1]:text-[11px] [&_h1]:font-semibold [&_h2]:my-1 [&_h2]:text-[11px] [&_h2]:font-semibold [&_h3]:my-1 [&_h3]:text-[11px] [&_h3]:font-semibold [&_pre]:my-1 [&_pre]:overflow-x-auto [&_pre]:rounded [&_pre]:bg-background/60 [&_pre]:p-1.5 [&_pre]:text-[10px] [&_code]:text-[10px]";
|
|
44
46
|
|
|
45
|
-
const
|
|
47
|
+
const MAX_NESTED_DEPTH = 12;
|
|
48
|
+
const MAX_INDENT_DEPTH = 4;
|
|
46
49
|
|
|
47
50
|
/** Props for {@link RecordPreview}. */
|
|
48
51
|
export interface RecordPreviewProps {
|
|
@@ -60,87 +63,274 @@ export function RecordPreview({ value, className }: RecordPreviewProps) {
|
|
|
60
63
|
return (
|
|
61
64
|
<div
|
|
62
65
|
className={cn(
|
|
63
|
-
"relative min-w-0
|
|
66
|
+
"relative min-w-0 [&_[data-slot=table-container]]:overflow-x-hidden",
|
|
64
67
|
className,
|
|
65
68
|
)}
|
|
66
69
|
>
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{raw ? (
|
|
85
|
-
<JsonBlock json={rawJson} className="rounded bg-background/40 p-2 pr-8 text-[11px]" />
|
|
86
|
-
) : rows.length === 0 ? (
|
|
87
|
-
<pre className="whitespace-pre-wrap break-words rounded bg-background/40 p-2 pr-8 font-mono text-[11px] leading-relaxed">
|
|
88
|
-
{rawJson}
|
|
89
|
-
</pre>
|
|
90
|
-
) : (
|
|
91
|
-
<RecordTable rows={rows} depth={0} />
|
|
92
|
-
)}
|
|
70
|
+
<div className="sticky top-1 z-10 float-right mr-1 h-0">
|
|
71
|
+
<Tooltip>
|
|
72
|
+
<TooltipTrigger asChild>
|
|
73
|
+
<Button
|
|
74
|
+
type="button"
|
|
75
|
+
size="icon"
|
|
76
|
+
variant="ghost"
|
|
77
|
+
aria-pressed={raw}
|
|
78
|
+
aria-label={raw ? "Show table" : "Show JSON"}
|
|
79
|
+
className="size-6 bg-background/85 backdrop-blur-sm"
|
|
80
|
+
onClick={() => setRaw((current) => !current)}
|
|
81
|
+
>
|
|
82
|
+
{raw ? <Table2Icon className="size-3" /> : <BracesIcon className="size-3" />}
|
|
83
|
+
</Button>
|
|
84
|
+
</TooltipTrigger>
|
|
85
|
+
<TooltipContent>{raw ? "Table" : "JSON"}</TooltipContent>
|
|
86
|
+
</Tooltip>
|
|
93
87
|
</div>
|
|
88
|
+
{raw ? (
|
|
89
|
+
<JsonBlock json={rawJson} className="rounded bg-background/40 p-2 pr-8 text-[11px]" />
|
|
90
|
+
) : rows.length === 0 ? (
|
|
91
|
+
<pre className="whitespace-pre-wrap break-words rounded bg-background/40 p-2 pr-8 font-mono text-[11px] leading-relaxed">
|
|
92
|
+
{rawJson}
|
|
93
|
+
</pre>
|
|
94
|
+
) : (
|
|
95
|
+
<RecordTable rows={rows} />
|
|
96
|
+
)}
|
|
94
97
|
</div>
|
|
95
98
|
);
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
function RecordTable({ rows
|
|
101
|
+
function RecordTable({ rows }: { rows: RecordPreviewRow[] }) {
|
|
99
102
|
if (rows.length === 0) return null;
|
|
100
103
|
return (
|
|
101
|
-
<
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
104
|
+
<div className="min-w-0 px-2 pb-2">
|
|
105
|
+
<Table className="table-fixed text-[11px]">
|
|
106
|
+
<TableBody>
|
|
107
|
+
{rows.map((row) => (
|
|
108
|
+
<RecordRow key={row.key} row={row} depth={0} path={row.key} />
|
|
109
|
+
))}
|
|
110
|
+
</TableBody>
|
|
111
|
+
</Table>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function RecordRow({
|
|
117
|
+
row,
|
|
118
|
+
depth,
|
|
119
|
+
path,
|
|
120
|
+
divided,
|
|
121
|
+
}: {
|
|
122
|
+
row: RecordPreviewRow;
|
|
123
|
+
depth: number;
|
|
124
|
+
path: string;
|
|
125
|
+
divided?: boolean;
|
|
126
|
+
}) {
|
|
127
|
+
const nested = nestedRecordValue(row.value);
|
|
128
|
+
if (!nested) {
|
|
129
|
+
return <FieldRow row={row} depth={depth} divided={divided} />;
|
|
130
|
+
}
|
|
131
|
+
return (
|
|
132
|
+
<Fragment>
|
|
133
|
+
<SectionRow
|
|
134
|
+
label={row.label}
|
|
135
|
+
depth={depth}
|
|
136
|
+
divided={divided}
|
|
137
|
+
parsedFromText={nested.parsedFromText}
|
|
138
|
+
/>
|
|
139
|
+
<NestedRows value={nested.value} depth={depth + 1} path={path} />
|
|
140
|
+
</Fragment>
|
|
137
141
|
);
|
|
138
142
|
}
|
|
139
143
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
function FieldRow({
|
|
145
|
+
row,
|
|
146
|
+
depth,
|
|
147
|
+
divided,
|
|
148
|
+
}: {
|
|
149
|
+
row: RecordPreviewRow;
|
|
150
|
+
depth: number;
|
|
151
|
+
divided?: boolean;
|
|
152
|
+
}) {
|
|
153
|
+
return (
|
|
154
|
+
<TableRow className={cn("hover:bg-transparent", divided && "border-t-2 border-border/50")}>
|
|
155
|
+
<TableCell
|
|
156
|
+
colSpan={2}
|
|
157
|
+
className="min-w-0 whitespace-normal px-0 py-1.5 pr-8 align-top [overflow-wrap:anywhere]"
|
|
158
|
+
style={{ paddingLeft: rowIndent(depth) }}
|
|
159
|
+
>
|
|
160
|
+
<div className="grid min-w-0 gap-0.5 sm:grid-cols-[8.5rem_minmax(0,1fr)] sm:gap-2">
|
|
161
|
+
<div className="text-[10px] font-medium leading-snug text-muted-foreground sm:text-[11px]">
|
|
162
|
+
{row.label}
|
|
163
|
+
</div>
|
|
164
|
+
<div className="min-w-0">
|
|
165
|
+
<RecordValue value={row.value} depth={depth} />
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</TableCell>
|
|
169
|
+
</TableRow>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function SectionRow({
|
|
174
|
+
label,
|
|
175
|
+
depth,
|
|
176
|
+
divided,
|
|
177
|
+
parsedFromText,
|
|
178
|
+
}: {
|
|
179
|
+
label: string;
|
|
180
|
+
depth: number;
|
|
181
|
+
divided?: boolean;
|
|
182
|
+
parsedFromText: boolean;
|
|
183
|
+
}) {
|
|
184
|
+
return (
|
|
185
|
+
<TableRow
|
|
186
|
+
className={cn("border-b-0 hover:bg-transparent", divided && "border-t-2 border-border/50")}
|
|
187
|
+
>
|
|
188
|
+
<TableHead
|
|
189
|
+
colSpan={2}
|
|
190
|
+
className="h-auto px-0 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-wide text-foreground/85"
|
|
191
|
+
style={{ paddingLeft: rowIndent(depth) }}
|
|
192
|
+
>
|
|
193
|
+
<span className="inline-flex items-center gap-1">
|
|
194
|
+
{label}
|
|
195
|
+
{parsedFromText ? (
|
|
196
|
+
<BracesIcon
|
|
197
|
+
className="size-3 text-muted-foreground"
|
|
198
|
+
aria-label="JSON parsed from text"
|
|
199
|
+
/>
|
|
200
|
+
) : null}
|
|
201
|
+
</span>
|
|
202
|
+
</TableHead>
|
|
203
|
+
</TableRow>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function NestedRows({ value, depth, path }: { value: unknown; depth: number; path: string }) {
|
|
208
|
+
if (depth > MAX_NESTED_DEPTH) {
|
|
209
|
+
return <ValueRow value={value} depth={depth} />;
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(value)) {
|
|
212
|
+
return (
|
|
213
|
+
<Fragment>
|
|
214
|
+
{value.map((item, index) => (
|
|
215
|
+
<ArrayItemRows
|
|
216
|
+
key={`${path}.${index}`}
|
|
217
|
+
value={item}
|
|
218
|
+
depth={depth}
|
|
219
|
+
path={`${path}.${index}`}
|
|
220
|
+
divided={index > 0}
|
|
221
|
+
/>
|
|
222
|
+
))}
|
|
223
|
+
</Fragment>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (value != null && typeof value === "object") {
|
|
227
|
+
const rows = recordPreviewRows(value);
|
|
228
|
+
if (rows.length === 0) return <ValueRow value={value} depth={depth} />;
|
|
229
|
+
return (
|
|
230
|
+
<Fragment>
|
|
231
|
+
{rows.map((row) => (
|
|
232
|
+
<RecordRow
|
|
233
|
+
key={`${path}.${row.key}`}
|
|
234
|
+
row={row}
|
|
235
|
+
depth={depth}
|
|
236
|
+
path={`${path}.${row.key}`}
|
|
237
|
+
/>
|
|
238
|
+
))}
|
|
239
|
+
</Fragment>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return <ValueRow value={value} depth={depth} />;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function ArrayItemRows({
|
|
246
|
+
value,
|
|
247
|
+
depth,
|
|
248
|
+
path,
|
|
249
|
+
divided,
|
|
250
|
+
}: {
|
|
251
|
+
value: unknown;
|
|
252
|
+
depth: number;
|
|
253
|
+
path: string;
|
|
254
|
+
divided: boolean;
|
|
255
|
+
}) {
|
|
256
|
+
const nested = nestedRecordValue(value);
|
|
257
|
+
if (nested?.parsedFromText) {
|
|
258
|
+
return (
|
|
259
|
+
<Fragment>
|
|
260
|
+
<NestedTextMarkerRow depth={depth} divided={divided} />
|
|
261
|
+
<NestedRows value={nested.value} depth={depth + 1} path={path} />
|
|
262
|
+
</Fragment>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (value != null && typeof value === "object") {
|
|
266
|
+
const rows = recordPreviewRows(value);
|
|
267
|
+
if (rows.length === 0) return <ValueRow value={value} depth={depth} divided={divided} />;
|
|
268
|
+
return (
|
|
269
|
+
<Fragment>
|
|
270
|
+
{rows.map((row, index) => (
|
|
271
|
+
<RecordRow
|
|
272
|
+
key={`${path}.${row.key}`}
|
|
273
|
+
row={row}
|
|
274
|
+
depth={depth}
|
|
275
|
+
path={`${path}.${row.key}`}
|
|
276
|
+
divided={divided && index === 0}
|
|
277
|
+
/>
|
|
278
|
+
))}
|
|
279
|
+
</Fragment>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return <ValueRow value={value} depth={depth} divided={divided} />;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function NestedTextMarkerRow({ depth, divided }: { depth: number; divided: boolean }) {
|
|
286
|
+
return (
|
|
287
|
+
<TableRow className={cn("border-b-0 hover:bg-transparent", divided && "border-t-2")}>
|
|
288
|
+
<TableHead
|
|
289
|
+
colSpan={2}
|
|
290
|
+
className="h-auto px-0 pb-0.5 pt-2 text-muted-foreground"
|
|
291
|
+
style={{ paddingLeft: rowIndent(depth) }}
|
|
292
|
+
>
|
|
293
|
+
<BracesIcon className="size-3" aria-label="JSON parsed from text" />
|
|
294
|
+
</TableHead>
|
|
295
|
+
</TableRow>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function ValueRow({ value, depth, divided }: { value: unknown; depth: number; divided?: boolean }) {
|
|
300
|
+
return (
|
|
301
|
+
<TableRow className={cn("hover:bg-transparent", divided && "border-t-2 border-border/50")}>
|
|
302
|
+
<TableCell
|
|
303
|
+
colSpan={2}
|
|
304
|
+
className="min-w-0 whitespace-normal px-0 py-1.5 pr-8 align-top [overflow-wrap:anywhere]"
|
|
305
|
+
style={{ paddingLeft: rowIndent(depth) }}
|
|
306
|
+
>
|
|
307
|
+
<RecordValue value={value} depth={depth} />
|
|
308
|
+
</TableCell>
|
|
309
|
+
</TableRow>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function rowIndent(depth: number): string {
|
|
314
|
+
return `${Math.min(depth, MAX_INDENT_DEPTH) * 0.9}rem`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function nestedRecordValue(
|
|
318
|
+
value: unknown,
|
|
319
|
+
): { value: Record<string, unknown> | unknown[]; parsedFromText: boolean } | undefined {
|
|
320
|
+
const parsed = parseNestedJsonText(value);
|
|
321
|
+
if (parsed) return { value: parsed, parsedFromText: true };
|
|
322
|
+
if (value == null || typeof value !== "object") return undefined;
|
|
323
|
+
if (Array.isArray(value)) {
|
|
324
|
+
const hasNestedValue = value.some(
|
|
325
|
+
(item) =>
|
|
326
|
+
(item != null && typeof item === "object") || parseNestedJsonText(item) !== undefined,
|
|
327
|
+
);
|
|
328
|
+
if (!hasNestedValue) return undefined;
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
value: value as Record<string, unknown> | unknown[],
|
|
332
|
+
parsedFromText: false,
|
|
333
|
+
};
|
|
144
334
|
}
|
|
145
335
|
|
|
146
336
|
function RecordValue({ value, depth }: { value: unknown; depth: number }) {
|
|
@@ -151,6 +341,16 @@ function RecordValue({ value, depth }: { value: unknown; depth: number }) {
|
|
|
151
341
|
return <span className="tabular-nums">{String(value)}</span>;
|
|
152
342
|
}
|
|
153
343
|
if (typeof value === "string") {
|
|
344
|
+
const language = detectCodeLanguage(value);
|
|
345
|
+
if (language === "sql") {
|
|
346
|
+
return (
|
|
347
|
+
<HighlightedCodeBlock
|
|
348
|
+
source={value}
|
|
349
|
+
language={language}
|
|
350
|
+
className="rounded bg-background/60 p-1.5 text-[10px]"
|
|
351
|
+
/>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
154
354
|
if (looksLikeMarkdown(value)) {
|
|
155
355
|
return (
|
|
156
356
|
<div className={MARKDOWN_CELL_CLASSES}>
|
|
@@ -160,16 +360,13 @@ function RecordValue({ value, depth }: { value: unknown; depth: number }) {
|
|
|
160
360
|
}
|
|
161
361
|
return <span className="whitespace-pre-wrap break-words">{value}</span>;
|
|
162
362
|
}
|
|
163
|
-
if (typeof value === "object" && !Array.isArray(value) && depth < NESTED_DEPTH) {
|
|
164
|
-
return <RecordTable rows={recordPreviewRows(value)} depth={depth + 1} />;
|
|
165
|
-
}
|
|
166
363
|
if (Array.isArray(value)) {
|
|
167
364
|
if (value.every((item) => item == null || typeof item !== "object")) {
|
|
168
365
|
return (
|
|
169
366
|
<span className="whitespace-pre-wrap break-words">{value.map(String).join(", ")}</span>
|
|
170
367
|
);
|
|
171
368
|
}
|
|
172
|
-
if (depth <
|
|
369
|
+
if (depth < MAX_NESTED_DEPTH) {
|
|
173
370
|
return (
|
|
174
371
|
<div className="min-w-0 divide-y divide-border/60">
|
|
175
372
|
{value.map((item, index) => (
|