mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-29 19:35:20 +02:00
feat(web): unified json viewer/editor + edit existing automation
This commit is contained in:
parent
2d8d42bd9c
commit
fa0cdb9760
15 changed files with 504 additions and 119 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { FileJson } from "lucide-react";
|
||||
import React from "react";
|
||||
import { defaultStyles, JsonView } from "react-json-view-lite";
|
||||
import { JsonView } from "@/components/json-view";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
|
|
@ -10,7 +10,6 @@ import {
|
|||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import "react-json-view-lite/dist/index.css";
|
||||
|
||||
interface JsonMetadataViewerProps {
|
||||
title: string;
|
||||
|
|
@ -56,13 +55,13 @@ export function JsonMetadataViewer({
|
|||
{title} - Metadata
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-2 sm:mt-4 p-2 sm:p-4 bg-muted/30 rounded-md text-xs sm:text-sm">
|
||||
<div className="mt-2 sm:mt-4 p-2 sm:p-4 bg-muted/30 rounded-md text-xs sm:text-sm overflow-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spinner size="lg" className="text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<JsonView data={jsonData} style={defaultStyles} />
|
||||
<JsonView src={jsonData} collapsed={2} />
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
|
@ -87,8 +86,8 @@ export function JsonMetadataViewer({
|
|||
{title} - Metadata
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-2 sm:mt-4 p-2 sm:p-4 bg-muted/30 rounded-md text-xs sm:text-sm">
|
||||
<JsonView data={jsonData} style={defaultStyles} />
|
||||
<div className="mt-2 sm:mt-4 p-2 sm:p-4 bg-muted/30 rounded-md text-xs sm:text-sm overflow-auto">
|
||||
<JsonView src={jsonData} collapsed={2} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
93
surfsense_web/components/json-view.tsx
Normal file
93
surfsense_web/components/json-view.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"use client";
|
||||
|
||||
import ReactJson, { type InteractionProps } from "@microlink/react-json-view";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
/**
|
||||
* Shared JSON viewer/editor wrapper around @microlink/react-json-view.
|
||||
*
|
||||
* One component, dual mode: passing ``editable`` + ``onChange`` enables
|
||||
* inline value editing, key renaming, add and delete. Omitting them
|
||||
* yields a read-only viewer. The underlying library is uncontrolled — it
|
||||
* mutates its own internal copy of ``src`` and surfaces the final tree on
|
||||
* each interaction via ``updated_src``, which we forward to ``onChange``.
|
||||
*
|
||||
* Theme follows ``next-themes``: a dark base-16 palette in dark mode, the
|
||||
* library's neutral default in light mode. Defaults are tuned for our
|
||||
* compact UI surfaces (no data-type labels, no key quotes, triangle icons,
|
||||
* tight indent).
|
||||
*/
|
||||
export interface JsonViewProps {
|
||||
/** The JSON value to display. Primitives are wrapped under ``{ value }``
|
||||
* because the underlying library requires an object root. */
|
||||
src: unknown;
|
||||
/** Enables value/key editing + add + delete. Requires ``onChange`` to
|
||||
* observe the result; without it the toggle is silently a no-op. */
|
||||
editable?: boolean;
|
||||
/** Called with the full updated tree on every accepted interaction. */
|
||||
onChange?: (next: unknown) => void;
|
||||
/** Collapse depth. ``true`` collapses everything past the root; a number
|
||||
* collapses from that depth onward. */
|
||||
collapsed?: boolean | number;
|
||||
/** Root label. Default ``false`` (no label — saves vertical space). */
|
||||
name?: string | false;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DARK_THEME = "monokai" as const;
|
||||
const LIGHT_THEME = "rjv-default" as const;
|
||||
|
||||
const SHARED_DEFAULTS = {
|
||||
iconStyle: "triangle" as const,
|
||||
indentWidth: 2,
|
||||
enableClipboard: true,
|
||||
displayDataTypes: false,
|
||||
displayObjectSize: true,
|
||||
quotesOnKeys: false,
|
||||
collapseStringsAfterLength: 80,
|
||||
};
|
||||
|
||||
export function JsonView({
|
||||
src,
|
||||
editable = false,
|
||||
onChange,
|
||||
collapsed = 2,
|
||||
name = false,
|
||||
className,
|
||||
}: JsonViewProps) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const theme = resolvedTheme === "dark" ? DARK_THEME : LIGHT_THEME;
|
||||
|
||||
// The library throws on non-object roots. Wrap primitives and null/undefined.
|
||||
const safeSrc = useMemo(() => {
|
||||
if (src && typeof src === "object") return src as object;
|
||||
return { value: src };
|
||||
}, [src]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(interaction: InteractionProps) => {
|
||||
onChange?.(interaction.updated_src);
|
||||
return true;
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const interactive = editable && onChange ? handleChange : (false as const);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ReactJson
|
||||
src={safeSrc}
|
||||
name={name}
|
||||
theme={theme}
|
||||
collapsed={collapsed}
|
||||
onEdit={interactive}
|
||||
onAdd={interactive}
|
||||
onDelete={interactive}
|
||||
style={{ backgroundColor: "transparent", fontSize: 12, fontFamily: "var(--font-mono)" }}
|
||||
{...SHARED_DEFAULTS}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,17 +2,11 @@
|
|||
|
||||
import type { ToolCallMessagePartProps } from "@assistant-ui/react";
|
||||
import { useAtomValue } from "jotai";
|
||||
import {
|
||||
AlertCircle,
|
||||
Code,
|
||||
CornerDownLeftIcon,
|
||||
ExternalLink,
|
||||
Pencil,
|
||||
Workflow,
|
||||
} from "lucide-react";
|
||||
import { AlertCircle, CornerDownLeftIcon, ExternalLink, Pencil, Workflow } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
|
||||
import { JsonView } from "@/components/json-view";
|
||||
import { TextShimmerLoader } from "@/components/prompt-kit/loader";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { automationCreateRequest } from "@/contracts/types/automation.types";
|
||||
|
|
@ -231,28 +225,12 @@ interface JsonEditorProps {
|
|||
}
|
||||
|
||||
function JsonEditor({ initialValue, onSave, onCancel }: JsonEditorProps) {
|
||||
const [text, setText] = useState(() => JSON.stringify(initialValue, null, 2));
|
||||
const [value, setValue] = useState<Record<string, unknown>>(initialValue);
|
||||
const [issues, setIssues] = useState<string[]>([]);
|
||||
|
||||
function handleFormat() {
|
||||
try {
|
||||
setText(JSON.stringify(JSON.parse(text), null, 2));
|
||||
setIssues([]);
|
||||
} catch (err) {
|
||||
setIssues([`Cannot format — not valid JSON: ${(err as Error).message}`]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
setIssues([]);
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch (err) {
|
||||
setIssues([`Invalid JSON: ${(err as Error).message}`]);
|
||||
return;
|
||||
}
|
||||
const result = editArgsSchema.safeParse(parsed);
|
||||
const result = editArgsSchema.safeParse(value);
|
||||
if (!result.success) {
|
||||
setIssues(
|
||||
result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
||||
|
|
@ -264,14 +242,14 @@ function JsonEditor({ initialValue, onSave, onCancel }: JsonEditorProps) {
|
|||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
spellCheck={false}
|
||||
rows={16}
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-xs font-mono text-foreground shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring resize-y min-h-[12rem]"
|
||||
aria-label="Automation JSON"
|
||||
/>
|
||||
<div className="rounded-md border border-input bg-background px-3 py-2 max-h-[24rem] overflow-auto">
|
||||
<JsonView
|
||||
src={value}
|
||||
editable
|
||||
onChange={(next) => setValue(next as Record<string, unknown>)}
|
||||
collapsed={false}
|
||||
/>
|
||||
</div>
|
||||
{issues.length > 0 && (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2">
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium text-destructive">
|
||||
|
|
@ -291,10 +269,6 @@ function JsonEditor({ initialValue, onSave, onCancel }: JsonEditorProps) {
|
|||
<Button type="button" variant="ghost" size="sm" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" onClick={handleFormat}>
|
||||
<Code className="mr-1.5 h-3.5 w-3.5" />
|
||||
Format
|
||||
</Button>
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
Save edits
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue