diff --git a/apps/studio/components/interfaces/ReportsV2/editor/shared.ts b/apps/studio/components/interfaces/ReportsV2/editor/shared.ts index 7ac835f3857..7fbbfef91ba 100644 --- a/apps/studio/components/interfaces/ReportsV2/editor/shared.ts +++ b/apps/studio/components/interfaces/ReportsV2/editor/shared.ts @@ -1,3 +1,25 @@ import { NodeViewProps } from '@tiptap/react' export type TiptapNodeViewProps = NodeViewProps & { node: { attrs: T } } + +export const BLOCKS = [ + { + type: 'input', + label: 'Input', + }, + { + type: 'json', + label: 'JSON', + }, + { + type: 'table', + label: 'Table', + }, + { + type: 'chart', + label: 'Chart', + }, +] as const + +export type Block = (typeof BLOCKS)[number] +export type BlockType = Block['type'] diff --git a/apps/studio/components/interfaces/ReportsV2/editor/slash-commands.tsx b/apps/studio/components/interfaces/ReportsV2/editor/slash-commands.tsx index 1c5bb305101..3d18401f2d8 100644 --- a/apps/studio/components/interfaces/ReportsV2/editor/slash-commands.tsx +++ b/apps/studio/components/interfaces/ReportsV2/editor/slash-commands.tsx @@ -6,6 +6,8 @@ import { BarChart, Code, Database, + FileJsonIcon, + FormInputIcon, Heading1, Heading2, Heading3, @@ -66,6 +68,27 @@ export const SlashCommand = Extension.create({ const iconClassNames = 'h-3.5 w-3.5 text-zinc-500' const DEFAULT_SLASH_COMMANDS: SlashCommandItem[] = [ + { + title: 'Input', + searchTerms: ['input'], + icon: , + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).insertContent('').run() + }, + }, + { + title: 'JSON', + searchTerms: ['sql', 'json'], + icon: , + command: ({ editor, range }: CommandProps) => { + editor + .chain() + .focus() + .deleteRange(range) + .insertContent("

") + .run() + }, + }, { title: 'SQL Table', searchTerms: ['sql', 'table'], diff --git a/apps/studio/components/interfaces/ReportsV2/editor/supa-block.tsx b/apps/studio/components/interfaces/ReportsV2/editor/supa-block.tsx index 4b485af0396..accbdfa9ba8 100644 --- a/apps/studio/components/interfaces/ReportsV2/editor/supa-block.tsx +++ b/apps/studio/components/interfaces/ReportsV2/editor/supa-block.tsx @@ -3,34 +3,57 @@ import { Node } from '@tiptap/core' import { ReactNodeViewRenderer, NodeViewProps } from '@tiptap/react' import { useState } from 'react' import { NodeViewWrapper, mergeAttributes } from '@tiptap/react' -import { TiptapNodeViewProps } from './shared' +import { BlockType, TiptapNodeViewProps } from './shared' import DataGrid, { Column, RenderRowProps, Row } from 'react-data-grid' import BarChart from 'components/ui/Charts/BarChart' import { useParams } from 'common' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' -import { Button, cn } from 'ui' +import { Button, Input_Shadcn_, cn } from 'ui' import CodeEditor from 'components/ui/CodeEditor/CodeEditor' import { GenericSkeletonLoader } from 'ui-patterns' import { RefreshCwIcon, Trash2Icon } from 'lucide-react' +import { Label } from '@ui/components/shadcn/ui/label' +import { useRouter } from 'next/router' +import toast from 'react-hot-toast' -function SQLTableComponent(props: TiptapNodeViewProps<{ sql: string; type: 'table' | 'chart' }>) { +function SQLTableComponent(props: TiptapNodeViewProps<{ sql: string; type: BlockType }>) { const { node: { attrs: { sql, type }, }, } = props + const router = useRouter() const { project } = useProjectContext() const [innerSQL, setInnerSQL] = useState(sql) const [source, onSourceChange] = useState<'postgres' | 'logs'>('postgres') const [mode, setMode] = useState<'edit' | 'view'>('edit') const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + const [inputLabel, setInputLabel] = useState('') const { mutate: runQuery, isLoading, data } = useExecuteSqlMutation() function execute(sql: string) { + // Check if the SQL has any variables (starting with $) + const variables = sql.match(/\$\w+/g) + const values = variables?.map((variable) => router.query[variable.substring(1)] as string) + + if (variables && !values?.every((value) => value)) { + toast.error('Please provide all necessary variables') + return + } + + if (variables) { + // Replace the variables with the values from the URL + variables.forEach((variable) => { + sql = sql.replace(variable, router.query[variable.substring(1)] as string) + }) + } + + console.log(sql) + if (project) { runQuery({ sql, projectRef: project.ref, connectionString: project.connectionString }) } @@ -83,6 +106,7 @@ function SQLTableComponent(props: TiptapNodeViewProps<{ sql: string; type: 'tabl yAxisKey={columns[1]?.key || ''} /> )} + {type === 'json' &&
{JSON.stringify(data?.result, null, 2)}
} )} diff --git a/apps/studio/components/interfaces/ReportsV2/editor/supa-editor.tsx b/apps/studio/components/interfaces/ReportsV2/editor/supa-editor.tsx index 5ca69283fd1..a69e6d292b3 100644 --- a/apps/studio/components/interfaces/ReportsV2/editor/supa-editor.tsx +++ b/apps/studio/components/interfaces/ReportsV2/editor/supa-editor.tsx @@ -2,6 +2,7 @@ import { useEditor, EditorContent } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import { SupaBlock } from './supa-block' +import { SupaInput } from './supa-input' import GlobalDragHandle from 'tiptap-extension-global-drag-handle' import { SlashCommand, getSlashCommandSuggestions } from './slash-commands' import { TrailingNode } from './trailing-node' @@ -12,6 +13,7 @@ export function SupaEditor() { extensions: [ StarterKit, SupaBlock, + SupaInput, GlobalDragHandle, SlashCommand.configure({ suggestion: getSlashCommandSuggestions([]), diff --git a/apps/studio/components/interfaces/ReportsV2/editor/supa-input.tsx b/apps/studio/components/interfaces/ReportsV2/editor/supa-input.tsx new file mode 100644 index 00000000000..b679b612f2f --- /dev/null +++ b/apps/studio/components/interfaces/ReportsV2/editor/supa-input.tsx @@ -0,0 +1,100 @@ +'use client' +import { Node } from '@tiptap/core' +import { ReactNodeViewRenderer, NodeViewProps } from '@tiptap/react' +import { useState } from 'react' +import { NodeViewWrapper, mergeAttributes } from '@tiptap/react' +import { BlockType, TiptapNodeViewProps } from './shared' +import DataGrid, { Column, RenderRowProps, Row } from 'react-data-grid' +import BarChart from 'components/ui/Charts/BarChart' +import { useParams } from 'common' +import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' +import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' +import { Button, Input_Shadcn_, cn } from 'ui' +import CodeEditor from 'components/ui/CodeEditor/CodeEditor' +import { GenericSkeletonLoader } from 'ui-patterns' +import { RefreshCwIcon, Trash2Icon } from 'lucide-react' +import { Label } from '@ui/components/shadcn/ui/label' +import { useRouter } from 'next/router' + +function SupaInputComponent(props: TiptapNodeViewProps<{ name: string }>) { + const { + node: { + attrs: { name }, + }, + } = props + + // This input will have a label and a value, the value will be stored in the URL as a param + // Using the label, you can get the value of the input in SQL blocks like this ${label} + + const router = useRouter() + const [inputName, setInputName] = useState(name) + const [mode, setMode] = useState<'edit' | 'view'>('edit') + + return ( + + {mode === 'edit' ? ( +
{ + e.stopPropagation() + }} + className="flex flex-col mt-8 group gap-4" + > + +

This will be used to reference this input in your SQL queries.

+ setInputName(e.target.value)} /> + +
+ ) : ( +
+ + { + // Update URL param + router.push({ + ...router, + query: { + ...router.query, + [inputName]: e.target.value, + }, + }) + }} + /> +
+ )} +
+ ) +} + +const name = 'supa-input' +export const SupaInput = Node.create({ + name, + group: 'block', + + addAttributes() { + return { + name: { + default: '', + }, + } + }, + + parseHTML() { + return [{ tag: name }] + }, + + renderHTML({ HTMLAttributes }) { + return [name, mergeAttributes(HTMLAttributes)] + }, + + addNodeView() { + return ReactNodeViewRenderer(SupaInputComponent) + }, +})