@dbx-tools/ui 0.9.112 → 0.9.116
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.116";
|
|
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.116",
|
|
34
|
+
"@dbx-tools/shared-core": "^0.9.116",
|
|
35
|
+
"@dbx-tools/shared-email": "^0.9.116",
|
|
36
|
+
"@dbx-tools/shared-email-template": "^0.9.116",
|
|
37
|
+
"@dbx-tools/shared-search": "^0.9.116",
|
|
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.116",
|
|
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,270 @@ 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>
|
|
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
|
+
<TableHead
|
|
156
|
+
className="h-auto w-28 align-top whitespace-normal px-0 py-1.5 pr-2 font-medium text-muted-foreground sm:w-[8.5rem]"
|
|
157
|
+
style={{ paddingLeft: rowIndent(depth) }}
|
|
158
|
+
>
|
|
159
|
+
{row.label}
|
|
160
|
+
</TableHead>
|
|
161
|
+
<TableCell className="min-w-0 whitespace-normal px-0 py-1.5 pr-8 align-top [overflow-wrap:anywhere]">
|
|
162
|
+
<RecordValue value={row.value} depth={depth} />
|
|
163
|
+
</TableCell>
|
|
164
|
+
</TableRow>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function SectionRow({
|
|
169
|
+
label,
|
|
170
|
+
depth,
|
|
171
|
+
divided,
|
|
172
|
+
parsedFromText,
|
|
173
|
+
}: {
|
|
174
|
+
label: string;
|
|
175
|
+
depth: number;
|
|
176
|
+
divided?: boolean;
|
|
177
|
+
parsedFromText: boolean;
|
|
178
|
+
}) {
|
|
179
|
+
return (
|
|
180
|
+
<TableRow
|
|
181
|
+
className={cn("border-b-0 hover:bg-transparent", divided && "border-t-2 border-border/50")}
|
|
182
|
+
>
|
|
183
|
+
<TableHead
|
|
184
|
+
colSpan={2}
|
|
185
|
+
className="h-auto px-0 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-wide text-foreground/85"
|
|
186
|
+
style={{ paddingLeft: rowIndent(depth) }}
|
|
187
|
+
>
|
|
188
|
+
<span className="inline-flex items-center gap-1">
|
|
189
|
+
{label}
|
|
190
|
+
{parsedFromText ? (
|
|
191
|
+
<BracesIcon
|
|
192
|
+
className="size-3 text-muted-foreground"
|
|
193
|
+
aria-label="JSON parsed from text"
|
|
194
|
+
/>
|
|
195
|
+
) : null}
|
|
196
|
+
</span>
|
|
197
|
+
</TableHead>
|
|
198
|
+
</TableRow>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function NestedRows({ value, depth, path }: { value: unknown; depth: number; path: string }) {
|
|
203
|
+
if (depth > MAX_NESTED_DEPTH) {
|
|
204
|
+
return <ValueRow value={value} depth={depth} />;
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(value)) {
|
|
207
|
+
return (
|
|
208
|
+
<Fragment>
|
|
209
|
+
{value.map((item, index) => (
|
|
210
|
+
<ArrayItemRows
|
|
211
|
+
key={`${path}.${index}`}
|
|
212
|
+
value={item}
|
|
213
|
+
depth={depth}
|
|
214
|
+
path={`${path}.${index}`}
|
|
215
|
+
divided={index > 0}
|
|
216
|
+
/>
|
|
217
|
+
))}
|
|
218
|
+
</Fragment>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (value != null && typeof value === "object") {
|
|
222
|
+
const rows = recordPreviewRows(value);
|
|
223
|
+
if (rows.length === 0) return <ValueRow value={value} depth={depth} />;
|
|
224
|
+
return (
|
|
225
|
+
<Fragment>
|
|
226
|
+
{rows.map((row) => (
|
|
227
|
+
<RecordRow
|
|
228
|
+
key={`${path}.${row.key}`}
|
|
229
|
+
row={row}
|
|
230
|
+
depth={depth}
|
|
231
|
+
path={`${path}.${row.key}`}
|
|
232
|
+
/>
|
|
233
|
+
))}
|
|
234
|
+
</Fragment>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return <ValueRow value={value} depth={depth} />;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function ArrayItemRows({
|
|
241
|
+
value,
|
|
242
|
+
depth,
|
|
243
|
+
path,
|
|
244
|
+
divided,
|
|
245
|
+
}: {
|
|
246
|
+
value: unknown;
|
|
247
|
+
depth: number;
|
|
248
|
+
path: string;
|
|
249
|
+
divided: boolean;
|
|
250
|
+
}) {
|
|
251
|
+
const nested = nestedRecordValue(value);
|
|
252
|
+
if (nested?.parsedFromText) {
|
|
253
|
+
return (
|
|
254
|
+
<Fragment>
|
|
255
|
+
<NestedTextMarkerRow depth={depth} divided={divided} />
|
|
256
|
+
<NestedRows value={nested.value} depth={depth + 1} path={path} />
|
|
257
|
+
</Fragment>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (value != null && typeof value === "object") {
|
|
261
|
+
const rows = recordPreviewRows(value);
|
|
262
|
+
if (rows.length === 0) return <ValueRow value={value} depth={depth} divided={divided} />;
|
|
263
|
+
return (
|
|
264
|
+
<Fragment>
|
|
265
|
+
{rows.map((row, index) => (
|
|
266
|
+
<RecordRow
|
|
267
|
+
key={`${path}.${row.key}`}
|
|
268
|
+
row={row}
|
|
269
|
+
depth={depth}
|
|
270
|
+
path={`${path}.${row.key}`}
|
|
271
|
+
divided={divided && index === 0}
|
|
272
|
+
/>
|
|
273
|
+
))}
|
|
274
|
+
</Fragment>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return <ValueRow value={value} depth={depth} divided={divided} />;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function NestedTextMarkerRow({ depth, divided }: { depth: number; divided: boolean }) {
|
|
281
|
+
return (
|
|
282
|
+
<TableRow className={cn("border-b-0 hover:bg-transparent", divided && "border-t-2")}>
|
|
283
|
+
<TableHead
|
|
284
|
+
colSpan={2}
|
|
285
|
+
className="h-auto px-0 pb-0.5 pt-2 text-muted-foreground"
|
|
286
|
+
style={{ paddingLeft: rowIndent(depth) }}
|
|
287
|
+
>
|
|
288
|
+
<BracesIcon className="size-3" aria-label="JSON parsed from text" />
|
|
289
|
+
</TableHead>
|
|
290
|
+
</TableRow>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function ValueRow({ value, depth, divided }: { value: unknown; depth: number; divided?: boolean }) {
|
|
295
|
+
return (
|
|
296
|
+
<TableRow className={cn("hover:bg-transparent", divided && "border-t-2 border-border/50")}>
|
|
297
|
+
<TableHead
|
|
298
|
+
aria-hidden
|
|
299
|
+
className="h-auto w-28 px-0 py-1.5 pr-2 sm:w-[8.5rem]"
|
|
300
|
+
style={{ paddingLeft: rowIndent(depth) }}
|
|
301
|
+
/>
|
|
302
|
+
<TableCell className="min-w-0 whitespace-normal px-0 py-1.5 pr-8 align-top [overflow-wrap:anywhere]">
|
|
303
|
+
<RecordValue value={value} depth={depth} />
|
|
304
|
+
</TableCell>
|
|
305
|
+
</TableRow>
|
|
137
306
|
);
|
|
138
307
|
}
|
|
139
308
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
309
|
+
function rowIndent(depth: number): string {
|
|
310
|
+
return `${Math.min(depth, MAX_INDENT_DEPTH) * 0.9}rem`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function nestedRecordValue(
|
|
314
|
+
value: unknown,
|
|
315
|
+
): { value: Record<string, unknown> | unknown[]; parsedFromText: boolean } | undefined {
|
|
316
|
+
const parsed = parseNestedJsonText(value);
|
|
317
|
+
if (parsed) return { value: parsed, parsedFromText: true };
|
|
318
|
+
if (value == null || typeof value !== "object") return undefined;
|
|
319
|
+
if (Array.isArray(value)) {
|
|
320
|
+
const hasNestedValue = value.some(
|
|
321
|
+
(item) =>
|
|
322
|
+
(item != null && typeof item === "object") || parseNestedJsonText(item) !== undefined,
|
|
323
|
+
);
|
|
324
|
+
if (!hasNestedValue) return undefined;
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
value: value as Record<string, unknown> | unknown[],
|
|
328
|
+
parsedFromText: false,
|
|
329
|
+
};
|
|
144
330
|
}
|
|
145
331
|
|
|
146
332
|
function RecordValue({ value, depth }: { value: unknown; depth: number }) {
|
|
@@ -151,6 +337,16 @@ function RecordValue({ value, depth }: { value: unknown; depth: number }) {
|
|
|
151
337
|
return <span className="tabular-nums">{String(value)}</span>;
|
|
152
338
|
}
|
|
153
339
|
if (typeof value === "string") {
|
|
340
|
+
const language = detectCodeLanguage(value);
|
|
341
|
+
if (language === "sql") {
|
|
342
|
+
return (
|
|
343
|
+
<HighlightedCodeBlock
|
|
344
|
+
source={value}
|
|
345
|
+
language={language}
|
|
346
|
+
className="rounded bg-background/60 p-1.5 text-[10px]"
|
|
347
|
+
/>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
154
350
|
if (looksLikeMarkdown(value)) {
|
|
155
351
|
return (
|
|
156
352
|
<div className={MARKDOWN_CELL_CLASSES}>
|
|
@@ -160,16 +356,13 @@ function RecordValue({ value, depth }: { value: unknown; depth: number }) {
|
|
|
160
356
|
}
|
|
161
357
|
return <span className="whitespace-pre-wrap break-words">{value}</span>;
|
|
162
358
|
}
|
|
163
|
-
if (typeof value === "object" && !Array.isArray(value) && depth < NESTED_DEPTH) {
|
|
164
|
-
return <RecordTable rows={recordPreviewRows(value)} depth={depth + 1} />;
|
|
165
|
-
}
|
|
166
359
|
if (Array.isArray(value)) {
|
|
167
360
|
if (value.every((item) => item == null || typeof item !== "object")) {
|
|
168
361
|
return (
|
|
169
362
|
<span className="whitespace-pre-wrap break-words">{value.map(String).join(", ")}</span>
|
|
170
363
|
);
|
|
171
364
|
}
|
|
172
|
-
if (depth <
|
|
365
|
+
if (depth < MAX_NESTED_DEPTH) {
|
|
173
366
|
return (
|
|
174
367
|
<div className="min-w-0 divide-y divide-border/60">
|
|
175
368
|
{value.map((item, index) => (
|