Skip to content

Commit 380e59e

Browse files
committed
next: extended data now supports syntax highlighting and a cleaner ui
1 parent 76ebff5 commit 380e59e

File tree

4 files changed

+140
-41
lines changed

4 files changed

+140
-41
lines changed
Lines changed: 107 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
<script lang="ts">
2-
import CopyToClipboardButton from '$comp/copy-to-clipboard-button.svelte';
32
import ObjectDump from '$comp/object-dump.svelte';
4-
import { Code, H4 } from '$comp/typography';
3+
import { Code, CodeBlock, H4 } from '$comp/typography';
54
import { Button } from '$comp/ui/button';
5+
import * as DropdownMenu from '$comp/ui/dropdown-menu';
6+
import { isJSONString, isObject, isString, isXmlString } from '$features/shared/typing';
7+
import { UseClipboard } from '$lib/hooks/use-clipboard.svelte';
68
import ArrowDown from '@lucide/svelte/icons/arrow-down';
79
import ArrowUp from '@lucide/svelte/icons/arrow-up';
10+
import Copy from '@lucide/svelte/icons/copy';
11+
import MoreVertical from '@lucide/svelte/icons/more-vertical';
12+
import ToggleLeft from '@lucide/svelte/icons/toggle-left';
13+
import { toast } from 'svelte-sonner';
814
915
interface Props {
1016
canPromote?: boolean;
@@ -18,8 +24,24 @@
1824
1925
let { canPromote = true, data, demote = async () => {}, excludedKeys = [], isPromoted = false, promote = async () => {}, title }: Props = $props();
2026
21-
function getData(data: unknown, exclusions: string[]): unknown {
22-
if (typeof data !== 'object' || !(data instanceof Object)) {
27+
function transformData(data: unknown): unknown {
28+
if (isJSONString(data)) {
29+
try {
30+
return JSON.parse(data);
31+
} catch {
32+
return data;
33+
}
34+
}
35+
36+
return data;
37+
}
38+
39+
function getFilteredData(data: unknown, exclusions: string[]): unknown {
40+
if (Array.isArray(data)) {
41+
return data.map((item) => getFilteredData(item, exclusions));
42+
}
43+
44+
if (!isObject(data)) {
2345
return data;
2446
}
2547
@@ -32,60 +54,107 @@
3254
}, {});
3355
}
3456
35-
function hasFilteredData(data: unknown): boolean {
57+
function isEmpty(data: unknown): boolean {
3658
if (data === undefined || data === null) {
37-
return false;
59+
return true;
3860
}
3961
4062
if (Array.isArray(data)) {
41-
return data.length > 0;
63+
return data.length === 0;
64+
}
65+
66+
if (isObject(data)) {
67+
return Object.keys(data).length === 0;
4268
}
4369
44-
if (Object.prototype.toString.call(data) === '[object Object]') {
45-
return Object.keys(data).length > 0;
70+
if (isString(data)) {
71+
return data.trim().length === 0;
4672
}
4773
48-
return true;
74+
return false;
4975
}
5076
51-
function onToggleView(e: Event) {
52-
e.preventDefault();
77+
function onToggleView() {
5378
showRaw = !showRaw;
5479
}
5580
5681
let showRaw = $state(false);
57-
let filteredData = getData(data, excludedKeys);
58-
let hasData = hasFilteredData(filteredData);
59-
let json = data ? JSON.stringify(data, null, 2) : null;
82+
const transformedData = $derived(transformData(data));
83+
const filteredData = $derived(getFilteredData(transformedData, excludedKeys));
84+
const hasData = $derived(!isEmpty(transformedData));
85+
const showJSONCodeEditor = $derived(isJSONString(data) || Array.isArray(transformedData) || isObject(transformedData));
86+
const showXmlCodeEditor = $derived(isXmlString(filteredData));
87+
const canToggle = $derived(!showXmlCodeEditor);
88+
89+
const clipboardData = $derived(isString(data) ? data : JSON.stringify(data, null, 2));
90+
const code = $derived(isString(filteredData) ? filteredData : JSON.stringify(filteredData, null, 2));
91+
92+
const clipboard = new UseClipboard();
93+
async function copyToClipboard() {
94+
await clipboard.copy(clipboardData);
95+
if (clipboard.copied) {
96+
toast.success('Copy to clipboard succeeded');
97+
} else {
98+
toast.error('Copy to clipboard failed');
99+
}
100+
}
60101
</script>
61102

62103
{#if hasData}
63-
<div class="flex justify-between">
64-
<H4 class="mb-2">{title}</H4>
65-
<div class="flex justify-end gap-x-1">
66-
<Button onclick={onToggleView} variant="outline">Toggle View</Button>
67-
68-
<CopyToClipboardButton value={json}></CopyToClipboardButton>
69-
70-
{#if canPromote}
71-
{#if !isPromoted}
72-
<Button onclick={async () => await promote(title)} size="icon" title="Promote to Tab"
73-
><ArrowUp /><span class="sr-only">Promote to Tab</span></Button
74-
>
104+
<div class="flex flex-col space-y-2">
105+
<div class="flex items-center justify-between">
106+
<H4>{title}</H4>
107+
<DropdownMenu.Root>
108+
<DropdownMenu.Trigger>
109+
<Button variant="ghost" size="icon" title="Options">
110+
<MoreVertical class="size-4" />
111+
</Button>
112+
</DropdownMenu.Trigger>
113+
<DropdownMenu.Content align="end">
114+
<DropdownMenu.Group>
115+
<DropdownMenu.GroupHeading>Actions</DropdownMenu.GroupHeading>
116+
<DropdownMenu.Separator />
117+
{#if canToggle}
118+
<DropdownMenu.Item onclick={onToggleView} title="Toggle between raw and structured view">
119+
<ToggleLeft class="mr-2 size-4" />
120+
Toggle View
121+
</DropdownMenu.Item>
122+
{/if}
123+
<DropdownMenu.Item onclick={copyToClipboard} title="Copy to clipboard">
124+
<Copy class="mr-2 size-4" />
125+
Copy to Clipboard
126+
</DropdownMenu.Item>
127+
{#if canPromote}
128+
{#if !isPromoted}
129+
<DropdownMenu.Item onclick={async () => await promote(title)} title="Promote to Tab">
130+
<ArrowUp class="mr-2 size-4" />
131+
Promote to Tab
132+
</DropdownMenu.Item>
133+
{:else}
134+
<DropdownMenu.Item onclick={async () => await demote(title)} title="Demote Tab">
135+
<ArrowDown class="mr-2 size-4" />
136+
Demote Tab
137+
</DropdownMenu.Item>
138+
{/if}
139+
{/if}
140+
</DropdownMenu.Group>
141+
</DropdownMenu.Content>
142+
</DropdownMenu.Root>
143+
</div>
144+
145+
<div class="grow overflow-auto text-xs">
146+
{#if showRaw || !canToggle}
147+
{#if showJSONCodeEditor}
148+
<CodeBlock {code} language="json" />
149+
{:else if showXmlCodeEditor}
150+
<CodeBlock {code} language="xml" />
75151
{:else}
76-
<Button onclick={async () => await demote(title)} size="icon" title="Demote Tab"
77-
><ArrowDown /><span class="sr-only">Demote Tab</span></Button
78-
>
152+
<pre class="bg-muted rounded p-2 break-words whitespace-pre-wrap"><Code class="px-0"><div class="bg-inherit">{clipboardData}</div></Code
153+
></pre>
79154
{/if}
155+
{:else}
156+
<ObjectDump value={filteredData} />
80157
{/if}
81158
</div>
82159
</div>
83-
84-
<div class="grow overflow-auto text-xs">
85-
{#if showRaw}
86-
<pre class="bg-muted rounded p-2 break-words whitespace-pre-wrap"><Code class="px-0"><div class="bg-inherit">{json}</div></Code></pre>
87-
{:else}
88-
<ObjectDump value={filteredData} />
89-
{/if}
90-
</div>
91160
{/if}

src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/copy-to-clipboard-button.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
children?: Snippet;
1212
size?: VariantProps<typeof buttonVariants>['size'];
1313
value?: null | string;
14+
variant?: VariantProps<typeof buttonVariants>['variant'];
1415
};
1516
16-
let { children, size = 'icon', title = 'Copy to Clipboard', value }: Props = $props();
17+
let { children, size = 'icon', title = 'Copy to Clipboard', value, variant = 'default' }: Props = $props();
1718
1819
const clipboard = new UseClipboard();
1920
@@ -28,7 +29,7 @@
2829
</script>
2930

3031
<div>
31-
<Button onclick={copyToClipboard} {size} {title}>
32+
<Button onclick={copyToClipboard} {size} {title} {variant}>
3233
{#if children}
3334
{@render children()}
3435
{:else}

src/Exceptionless.Web/ClientApp/src/lib/features/shared/components/object-dump.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
{#if isEmptyValue}
4444
(Empty)
4545
{:else if Array.isArray(value)}
46-
<List items={value}>
46+
<List items={value} class="my-0">
4747
{#snippet displayValue(item)}
4848
<ObjectDump value={item} />
4949
{/snippet}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export function isJSONString(value: unknown): value is string {
2+
if (!isString(value)) {
3+
return false;
4+
}
5+
6+
try {
7+
JSON.parse(value);
8+
return true;
9+
} catch {
10+
return false;
11+
}
12+
}
13+
14+
export function isObject(value: unknown): value is Record<string, unknown> {
15+
return Object.prototype.toString.call(value) === '[object Object]';
16+
}
17+
18+
export function isString(value: unknown): value is string {
19+
return Object.prototype.toString.call(value) === '[object String]';
20+
}
21+
22+
export function isXmlString(value: unknown): value is string {
23+
if (!isString(value)) {
24+
return false;
25+
}
26+
27+
const trimmedValue = value.trim();
28+
return trimmedValue.startsWith('<') && trimmedValue.endsWith('>');
29+
}

0 commit comments

Comments
 (0)