77 | >(({ className, ...props }, ref) => (
78 | [role=checkbox]]:translate-y-[2px]',
82 | className
83 | )}
84 | {...props}
85 | />
86 | ))
87 | TableCell.displayName = 'TableCell'
88 |
89 | const TableCaption = React.forwardRef<
90 | HTMLTableCaptionElement,
91 | React.HTMLAttributes
92 | >(({ className, ...props }, ref) => (
93 |
94 | ))
95 | TableCaption.displayName = 'TableCaption'
96 |
97 | export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
98 |
--------------------------------------------------------------------------------
/src/renderer/src/components/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@renderer/lib/cn'
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes & {
8 | wrapperClassName?: string
9 | }
10 | >(({ className, wrapperClassName, ...props }, ref) => (
11 |
14 | ))
15 | Table.displayName = 'Table'
16 |
17 | const TableHeader = React.forwardRef<
18 | HTMLTableSectionElement,
19 | React.HTMLAttributes
20 | >(({ className, ...props }, ref) => (
21 |
22 | ))
23 | TableHeader.displayName = 'TableHeader'
24 |
25 | const TableBody = React.forwardRef<
26 | HTMLTableSectionElement,
27 | React.HTMLAttributes
28 | >(({ className, ...props }, ref) => (
29 |
30 | ))
31 | TableBody.displayName = 'TableBody'
32 |
33 | const TableFooter = React.forwardRef<
34 | HTMLTableSectionElement,
35 | React.HTMLAttributes
36 | >(({ className, ...props }, ref) => (
37 | tr]:last:border-b-0', className)}
40 | {...props}
41 | />
42 | ))
43 | TableFooter.displayName = 'TableFooter'
44 |
45 | const TableRow = React.forwardRef>(
46 | ({ className, ...props }, ref) => (
47 |
55 | )
56 | )
57 | TableRow.displayName = 'TableRow'
58 |
59 | const TableHead = React.forwardRef<
60 | HTMLTableCellElement,
61 | React.ThHTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 | [role=checkbox]]:translate-y-[2px]',
67 | className
68 | )}
69 | {...props}
70 | />
71 | ))
72 | TableHead.displayName = 'TableHead'
73 |
74 | const TableCell = React.forwardRef<
75 | HTMLTableCellElement,
76 | React.TdHTMLAttributes
77 | >(({ className, ...props }, ref) => (
78 | [role=checkbox]]:translate-y-[2px]',
82 | className
83 | )}
84 | {...props}
85 | />
86 | ))
87 | TableCell.displayName = 'TableCell'
88 |
89 | const TableCaption = React.forwardRef<
90 | HTMLTableCaptionElement,
91 | React.HTMLAttributes
92 | >(({ className, ...props }, ref) => (
93 |
94 | ))
95 | TableCaption.displayName = 'TableCaption'
96 |
97 | export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
98 |
--------------------------------------------------------------------------------
/src/renderer/src/lib/openai.ts:
--------------------------------------------------------------------------------
1 | import { Config, ConnectionType } from '@shared/types'
2 | import { generateText } from 'ai'
3 | import { createAnthropic } from '@ai-sdk/anthropic'
4 | import { createOpenAI } from '@ai-sdk/openai'
5 | import { ModelId, models } from '@shared/constants'
6 |
7 | const createProvider = (config: Config, model: ModelId) => {
8 | if (model.startsWith('claude-')) {
9 | if (!config.anthropicApiKey) {
10 | throw new Error('Missing Anthropic API Key')
11 | }
12 |
13 | return createAnthropic({
14 | apiKey: config.anthropicApiKey,
15 | baseURL: config.anthropicApiEndpoint
16 | })
17 | }
18 |
19 | if (model.startsWith('deepseek')) {
20 | if (!config.deepseekApiKey) {
21 | throw new Error('Missing DeepSeek API Key')
22 | }
23 |
24 | return createOpenAI({
25 | apiKey: config.deepseekApiKey,
26 | baseURL: "https://api.deepseek.com"
27 | })
28 | }
29 |
30 |
31 | if (!config.openaiApiKey) {
32 | throw new Error('Missing OpenAI API Key')
33 | }
34 |
35 | return createOpenAI({
36 | baseURL: config.openaiApiEndpoint,
37 | apiKey: config.openaiApiKey || ''
38 | })
39 | }
40 |
41 | const sendMessages = async ({
42 | config,
43 | messages,
44 | maxTokens
45 | }: {
46 | config: Config
47 | messages: { role: 'user' | 'assistant' | 'system'; content: string }[]
48 | maxTokens?: number
49 | }) => {
50 | const modelId = config.model || 'gpt-3.5-turbo'
51 |
52 | const provider = createProvider(config, modelId)
53 |
54 | const model = models.find((m) => m.value === modelId)!
55 |
56 | const result = await generateText({
57 | model: provider(model.realModelId || model.value),
58 | maxTokens: maxTokens || 1024,
59 | messages,
60 | temperature: 0
61 | })
62 |
63 | return { content: result.text }
64 | }
65 |
66 | export const generateSQL = async (options: {
67 | connecttionType: ConnectionType
68 | config: Config
69 | schema: string
70 | input: string
71 | }) => {
72 | const content = [
73 | `Database type: ${options.connecttionType}\n`,
74 | options.schema ? `Database schema:\n${options.schema}\n\n` : '',
75 | `Generate an SQL query${options.schema ? ' based on provided schema' : ''} for the question: ${options.input}\n\nReturn the SQL directly without any other text, do NOT wrap in code block, table name and column name should be escaped with quotes.`
76 | ].join('')
77 |
78 | const output = await sendMessages({
79 | config: options.config,
80 | messages: [{ role: 'user', content: content }]
81 | })
82 |
83 | return output
84 | }
85 |
86 | export const generateFixForError = async (options: {
87 | type: ConnectionType
88 | error: string
89 | query: string
90 | config: Config
91 | }) => {
92 | const content = [
93 | ` Generate a fix for the following error when querying a ${options.type} database:\n\n${options.error}\n\nThe query that caused the error is:\n\n${options.query}\n\nReturn in the following forma:\nCorrect query:\n\`\`\`\n\n\`\`\`\n\n`
94 | ].join('')
95 |
96 | const output = await sendMessages({
97 | config: options.config,
98 | messages: [{ role: 'user', content: content }]
99 | })
100 |
101 | return output
102 | }
103 |
--------------------------------------------------------------------------------
/src/renderer/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { tv, VariantProps } from 'tailwind-variants'
2 | import { forwardRef } from 'react'
3 | import { Spinner } from './spinner'
4 |
5 | export const buttonVariants = tv({
6 | base: 'inline-flex shrink-0 items-center justify-center select-none rounded-md border text-center text-sm outline-none ring-blue-500 focus:border-blue-500 focus:ring-1 disabled:pointer-events-none disabled:opacity-50',
7 | variants: {
8 | variant: {
9 | default: '',
10 | primary:
11 | 'h-10 border-blue-500 border-b-blue-800 bg-blue-500 px-4 text-white active:bg-blue-600',
12 | outline:
13 | 'h-10 border-zinc-300 border-b-zinc-400/80 bg-white px-4 dark:border-zinc-600 dark:bg-transparent',
14 | ghost: 'border border-transparent hover:bg-zinc-200 dark:hover:bg-zinc-700/60',
15 | message_action: 'focus:ring-2'
16 | },
17 | variantColor: {
18 | default: '',
19 | red: ''
20 | },
21 | size: {
22 | default: 'h-8 px-3',
23 | sm: 'px-2 h-6'
24 | },
25 | isIcon: {
26 | true: ''
27 | }
28 | },
29 | compoundVariants: [
30 | {
31 | variant: 'default',
32 | variantColor: 'default',
33 | className:
34 | 'border-zinc-300 border-b-zinc-400/80 bg-white active:bg-zinc-200 dark:border-zinc-600 dark:bg-zinc-800 dark:active:bg-zinc-900'
35 | },
36 | {
37 | variant: 'default',
38 | variantColor: 'red',
39 | className:
40 | 'border-red-500 border-b-red-600 text-red-500 focus:ring-red-300 active:bg-red-100 dark:bg-red-900/30 dark:focus:bg-red-900/20 dark:focus:ring-red-600'
41 | },
42 | {
43 | variant: 'primary',
44 | variantColor: 'red',
45 | className: 'border-red-500 bg-red-500 focus:bg-red-600 focus:ring-red-300 active:bg-red-600'
46 | },
47 | {
48 | variant: 'message_action',
49 | variantColor: 'default',
50 | className:
51 | 'border-0 hover:bg-zinc-200 aria-expanded:bg-zinc-200 dark:hover:bg-zinc-800 dark:aria-expanded:bg-zinc-800'
52 | },
53 |
54 | {
55 | size: 'default',
56 | isIcon: true,
57 | class: 'px-0 w-8'
58 | }
59 | ],
60 | defaultVariants: {
61 | variant: 'default',
62 | variantColor: 'default',
63 | size: 'default'
64 | }
65 | })
66 |
67 | type Props = React.ButtonHTMLAttributes &
68 | VariantProps & {
69 | isLoading?: boolean
70 | left?: React.ReactNode
71 | }
72 |
73 | export const Button = forwardRef(
74 | (
75 | {
76 | className,
77 | variant,
78 | disabled,
79 | isLoading,
80 | children,
81 | left,
82 | size,
83 | isIcon,
84 | variantColor,
85 | ...props
86 | },
87 | ref
88 | ) => {
89 | const disable = disabled || isLoading
90 | return (
91 |
107 | )
108 | }
109 | )
110 |
--------------------------------------------------------------------------------
/src/renderer/src/components/button.tsx:
--------------------------------------------------------------------------------
1 | import { tv, VariantProps } from 'tailwind-variants'
2 | import { forwardRef } from 'react'
3 | import { Spinner } from './spinner'
4 |
5 | export const buttonVariants = tv({
6 | base: 'inline-flex shrink-0 items-center justify-center select-none rounded-md border text-center text-sm outline-none ring-blue-500 focus:border-blue-500 focus:ring-1 disabled:pointer-events-none disabled:opacity-50',
7 | variants: {
8 | variant: {
9 | default: '',
10 | primary:
11 | 'h-10 border-blue-500 border-b-blue-800 bg-blue-500 px-4 text-white active:bg-blue-600',
12 | outline:
13 | 'h-10 border-zinc-300 border-b-zinc-400/80 bg-white px-4 dark:border-zinc-600 dark:bg-transparent',
14 | ghost: 'border border-transparent hover:bg-zinc-200 dark:hover:bg-zinc-700/60',
15 | message_action: 'focus:ring-2'
16 | },
17 | variantColor: {
18 | default: '',
19 | red: ''
20 | },
21 | size: {
22 | default: 'h-8 px-3',
23 | sm: 'px-2 h-6'
24 | },
25 | isIcon: {
26 | true: ''
27 | }
28 | },
29 | compoundVariants: [
30 | {
31 | variant: 'default',
32 | variantColor: 'default',
33 | className:
34 | 'border-zinc-300 border-b-zinc-400/80 bg-white active:bg-zinc-200 dark:border-zinc-600 dark:bg-zinc-800 dark:active:bg-zinc-900'
35 | },
36 | {
37 | variant: 'default',
38 | variantColor: 'red',
39 | className:
40 | 'border-red-500 border-b-red-600 text-red-500 focus:ring-red-300 active:bg-red-100 dark:bg-red-900/30 dark:focus:bg-red-900/20 dark:focus:ring-red-600'
41 | },
42 | {
43 | variant: 'primary',
44 | variantColor: 'red',
45 | className: 'border-red-500 bg-red-500 focus:bg-red-600 focus:ring-red-300 active:bg-red-600'
46 | },
47 | {
48 | variant: 'message_action',
49 | variantColor: 'default',
50 | className:
51 | 'border-0 hover:bg-zinc-200 aria-expanded:bg-zinc-200 dark:hover:bg-zinc-800 dark:aria-expanded:bg-zinc-800'
52 | },
53 |
54 | {
55 | size: 'default',
56 | isIcon: true,
57 | class: 'px-0 w-8'
58 | }
59 | ],
60 | defaultVariants: {
61 | variant: 'default',
62 | variantColor: 'default',
63 | size: 'default'
64 | }
65 | })
66 |
67 | type Props = React.ButtonHTMLAttributes &
68 | VariantProps & {
69 | isLoading?: boolean
70 | left?: React.ReactNode
71 | }
72 |
73 | export const Button = forwardRef(
74 | (
75 | {
76 | className,
77 | variant,
78 | disabled,
79 | isLoading,
80 | children,
81 | left,
82 | size,
83 | isIcon,
84 | variantColor,
85 | ...props
86 | },
87 | ref
88 | ) => {
89 | const disable = disabled || isLoading
90 | return (
91 |
107 | )
108 | }
109 | )
110 |
--------------------------------------------------------------------------------
/src/renderer/src/css/spinner.css:
--------------------------------------------------------------------------------
1 | .spinner {
2 | font-size: 1em;
3 | position: relative;
4 | display: inline-block;
5 | width: 1em;
6 | height: 1em;
7 | }
8 |
9 | .spinner .spinner-blade {
10 | position: absolute;
11 | left: 0.4629em;
12 | bottom: 0;
13 | width: 0.074em;
14 | height: 0.2777em;
15 | border-radius: 0.0555em;
16 | background-color: transparent;
17 | -webkit-transform-origin: center -0.2222em;
18 | -ms-transform-origin: center -0.2222em;
19 | transform-origin: center -0.2222em;
20 | animation: spinner-fade9234 1s infinite linear;
21 | }
22 |
23 | .spinner .spinner-blade:nth-child(1) {
24 | -webkit-animation-delay: 0s;
25 | animation-delay: 0s;
26 | -webkit-transform: rotate(0deg);
27 | -ms-transform: rotate(0deg);
28 | transform: rotate(0deg);
29 | }
30 |
31 | .spinner .spinner-blade:nth-child(2) {
32 | -webkit-animation-delay: 0.083s;
33 | animation-delay: 0.083s;
34 | -webkit-transform: rotate(30deg);
35 | -ms-transform: rotate(30deg);
36 | transform: rotate(30deg);
37 | }
38 |
39 | .spinner .spinner-blade:nth-child(3) {
40 | -webkit-animation-delay: 0.166s;
41 | animation-delay: 0.166s;
42 | -webkit-transform: rotate(60deg);
43 | -ms-transform: rotate(60deg);
44 | transform: rotate(60deg);
45 | }
46 |
47 | .spinner .spinner-blade:nth-child(4) {
48 | -webkit-animation-delay: 0.249s;
49 | animation-delay: 0.249s;
50 | -webkit-transform: rotate(90deg);
51 | -ms-transform: rotate(90deg);
52 | transform: rotate(90deg);
53 | }
54 |
55 | .spinner .spinner-blade:nth-child(5) {
56 | -webkit-animation-delay: 0.332s;
57 | animation-delay: 0.332s;
58 | -webkit-transform: rotate(120deg);
59 | -ms-transform: rotate(120deg);
60 | transform: rotate(120deg);
61 | }
62 |
63 | .spinner .spinner-blade:nth-child(6) {
64 | -webkit-animation-delay: 0.415s;
65 | animation-delay: 0.415s;
66 | -webkit-transform: rotate(150deg);
67 | -ms-transform: rotate(150deg);
68 | transform: rotate(150deg);
69 | }
70 |
71 | .spinner .spinner-blade:nth-child(7) {
72 | -webkit-animation-delay: 0.498s;
73 | animation-delay: 0.498s;
74 | -webkit-transform: rotate(180deg);
75 | -ms-transform: rotate(180deg);
76 | transform: rotate(180deg);
77 | }
78 |
79 | .spinner .spinner-blade:nth-child(8) {
80 | -webkit-animation-delay: 0.581s;
81 | animation-delay: 0.581s;
82 | -webkit-transform: rotate(210deg);
83 | -ms-transform: rotate(210deg);
84 | transform: rotate(210deg);
85 | }
86 |
87 | .spinner .spinner-blade:nth-child(9) {
88 | -webkit-animation-delay: 0.664s;
89 | animation-delay: 0.664s;
90 | -webkit-transform: rotate(240deg);
91 | -ms-transform: rotate(240deg);
92 | transform: rotate(240deg);
93 | }
94 |
95 | .spinner .spinner-blade:nth-child(10) {
96 | -webkit-animation-delay: 0.747s;
97 | animation-delay: 0.747s;
98 | -webkit-transform: rotate(270deg);
99 | -ms-transform: rotate(270deg);
100 | transform: rotate(270deg);
101 | }
102 |
103 | .spinner .spinner-blade:nth-child(11) {
104 | -webkit-animation-delay: 0.83s;
105 | animation-delay: 0.83s;
106 | -webkit-transform: rotate(300deg);
107 | -ms-transform: rotate(300deg);
108 | transform: rotate(300deg);
109 | }
110 |
111 | .spinner .spinner-blade:nth-child(12) {
112 | -webkit-animation-delay: 0.913s;
113 | animation-delay: 0.913s;
114 | -webkit-transform: rotate(330deg);
115 | -ms-transform: rotate(330deg);
116 | transform: rotate(330deg);
117 | }
118 |
119 | @keyframes spinner-fade9234 {
120 | 0% {
121 | background-color: currentColor;
122 | }
123 |
124 | 100% {
125 | background-color: transparent;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "querybase",
3 | "productName": "QueryBase",
4 | "version": "0.1.2",
5 | "type": "module",
6 | "description": "A simple and smart GUI for SQLite, MySQL, PostgreSQL",
7 | "main": "./out/main/index.js",
8 | "author": "Umida Inc.",
9 | "homepage": "https://umida.co",
10 | "scripts": {
11 | "format": "prettier --write .",
12 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
13 | "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
14 | "typecheck": "npm run typecheck:node && npm run typecheck:web",
15 | "start": "electron-vite preview",
16 | "dev": "electron-vite dev --watch",
17 | "build": "npm run typecheck && electron-vite build",
18 | "postinstall": "electron-builder install-app-deps",
19 | "build:unpack": "npm run build && electron-builder --dir",
20 | "build:win": "npm run build && electron-builder --win --config electron-builder.config.cjs",
21 | "build:mac": "electron-vite build && electron-builder --mac --config electron-builder.config.cjs",
22 | "build:linux": "electron-vite build && electron-builder --linux --config electron-builder.config.cjs",
23 | "release": "node ./scripts/release.js",
24 | "db-push": "drizzle-kit push",
25 | "db-gen": "drizzle-kit generate"
26 | },
27 | "dependencies": {
28 | "@electron-toolkit/preload": "^3.0.1",
29 | "@electron-toolkit/utils": "^3.0.0",
30 | "@libsql/client": "^0.14.0",
31 | "drizzle-orm": "^0.35.3",
32 | "electron-updater": "^6.3.9",
33 | "mysql2": "^3.9.3",
34 | "pg": "^8.13.1"
35 | },
36 | "devDependencies": {
37 | "@ai-sdk/anthropic": "^0.0.53",
38 | "@ai-sdk/openai": "^0.0.70",
39 | "@codemirror/lang-sql": "^6.6.2",
40 | "@egoist/tailwindcss-icons": "^1.7.4",
41 | "@electron-toolkit/tsconfig": "^1.0.1",
42 | "@fontsource-variable/jetbrains-mono": "^5.0.20",
43 | "@iconify-json/devicon": "^1.1.36",
44 | "@iconify-json/logos": "^1.1.42",
45 | "@iconify-json/lucide": "^1.1.178",
46 | "@iconify-json/material-symbols": "^1.1.76",
47 | "@iconify-json/mingcute": "^1.1.17",
48 | "@iconify-json/ri": "^1.1.20",
49 | "@iconify-json/tabler": "^1.1.109",
50 | "@radix-ui/react-dialog": "^1.0.5",
51 | "@radix-ui/react-dropdown-menu": "^2.0.6",
52 | "@radix-ui/react-popover": "^1.0.7",
53 | "@radix-ui/react-slot": "^1.0.2",
54 | "@radix-ui/react-tooltip": "^1.0.7",
55 | "@tailwindcss/typography": "^0.5.12",
56 | "@tanstack/react-query": "^5.28.9",
57 | "@tanstack/react-table": "^8.15.3",
58 | "@types/node": "^18.19.9",
59 | "@types/pg": "^8.11.10",
60 | "@types/react": "^18.2.48",
61 | "@types/react-dom": "^18.2.18",
62 | "@types/react-input-autosize": "^2.2.4",
63 | "@types/url-parse": "^1.4.11",
64 | "@uiw/codemirror-themes": "^4.21.25",
65 | "@uiw/react-codemirror": "^4.21.25",
66 | "@vitejs/plugin-react": "^4.3.3",
67 | "ai": "^3.4.27",
68 | "clsx": "^2.1.0",
69 | "drizzle-kit": "^0.26.2",
70 | "electron": "^31.7.3",
71 | "electron-builder": "^25.1.8",
72 | "electron-vite": "^2.3.0",
73 | "framer-motion": "^11.0.22",
74 | "nanoid": "^5.0.6",
75 | "prettier": "^3.2.5",
76 | "pretty-bytes": "^6.1.1",
77 | "react": "^18.2.0",
78 | "react-dom": "^18.2.0",
79 | "react-hook-form": "^7.51.2",
80 | "react-input-autosize": "^3.0.0",
81 | "react-markdown": "^9.0.1",
82 | "react-router-dom": "^6.22.3",
83 | "reactflow": "^11.10.4",
84 | "sql-query-identifier": "^2.7.0",
85 | "tailwind-variants": "^0.2.1",
86 | "tailwindcss": "^3.4.1",
87 | "typescript": "^5.3.3",
88 | "url-parse": "^1.5.10",
89 | "vite": "^5.4.10"
90 | },
91 | "packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4"
92 | }
93 |
--------------------------------------------------------------------------------
/migrations/meta/0000_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "6",
3 | "dialect": "sqlite",
4 | "tables": {
5 | "connection": {
6 | "name": "connection",
7 | "columns": {
8 | "id": {
9 | "name": "id",
10 | "type": "text",
11 | "primaryKey": true,
12 | "notNull": true,
13 | "autoincrement": false
14 | },
15 | "createdAt": {
16 | "name": "createdAt",
17 | "type": "integer",
18 | "primaryKey": false,
19 | "notNull": true,
20 | "autoincrement": false
21 | },
22 | "nickname": {
23 | "name": "nickname",
24 | "type": "text",
25 | "primaryKey": false,
26 | "notNull": true,
27 | "autoincrement": false
28 | },
29 | "type": {
30 | "name": "type",
31 | "type": "text",
32 | "primaryKey": false,
33 | "notNull": true,
34 | "autoincrement": false
35 | },
36 | "host": {
37 | "name": "host",
38 | "type": "text",
39 | "primaryKey": false,
40 | "notNull": false,
41 | "autoincrement": false
42 | },
43 | "port": {
44 | "name": "port",
45 | "type": "text",
46 | "primaryKey": false,
47 | "notNull": false,
48 | "autoincrement": false
49 | },
50 | "user": {
51 | "name": "user",
52 | "type": "text",
53 | "primaryKey": false,
54 | "notNull": false,
55 | "autoincrement": false
56 | },
57 | "config": {
58 | "name": "config",
59 | "type": "text",
60 | "primaryKey": false,
61 | "notNull": false,
62 | "autoincrement": false
63 | },
64 | "password": {
65 | "name": "password",
66 | "type": "text",
67 | "primaryKey": false,
68 | "notNull": false,
69 | "autoincrement": false
70 | },
71 | "database": {
72 | "name": "database",
73 | "type": "text",
74 | "primaryKey": false,
75 | "notNull": true,
76 | "autoincrement": false
77 | }
78 | },
79 | "indexes": {},
80 | "foreignKeys": {},
81 | "compositePrimaryKeys": {},
82 | "uniqueConstraints": {},
83 | "checkConstraints": {}
84 | },
85 | "query": {
86 | "name": "query",
87 | "columns": {
88 | "id": {
89 | "name": "id",
90 | "type": "text",
91 | "primaryKey": true,
92 | "notNull": true,
93 | "autoincrement": false
94 | },
95 | "createdAt": {
96 | "name": "createdAt",
97 | "type": "integer",
98 | "primaryKey": false,
99 | "notNull": true,
100 | "autoincrement": false
101 | },
102 | "connectionId": {
103 | "name": "connectionId",
104 | "type": "text",
105 | "primaryKey": false,
106 | "notNull": true,
107 | "autoincrement": false
108 | },
109 | "title": {
110 | "name": "title",
111 | "type": "text",
112 | "primaryKey": false,
113 | "notNull": true,
114 | "autoincrement": false
115 | },
116 | "query": {
117 | "name": "query",
118 | "type": "text",
119 | "primaryKey": false,
120 | "notNull": true,
121 | "autoincrement": false
122 | }
123 | },
124 | "indexes": {
125 | "query_connectionId_idx": {
126 | "name": "query_connectionId_idx",
127 | "columns": ["connectionId"],
128 | "isUnique": false
129 | }
130 | },
131 | "foreignKeys": {},
132 | "compositePrimaryKeys": {},
133 | "uniqueConstraints": {},
134 | "checkConstraints": {}
135 | }
136 | },
137 | "enums": {},
138 | "_meta": {
139 | "tables": {},
140 | "columns": {}
141 | },
142 | "id": "7f704888-9657-41f4-ab6a-c2e6f5dd45b8",
143 | "prevId": "00000000-0000-0000-0000-000000000000"
144 | }
145 |
--------------------------------------------------------------------------------
/src/renderer/src/components/Dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as DialogPrimitive from '@radix-ui/react-dialog'
3 |
4 | import { cn } from '@renderer/lib/cn'
5 |
6 | const Dialog = DialogPrimitive.Root
7 |
8 | const DialogTrigger = DialogPrimitive.Trigger
9 |
10 | const DialogPortal = DialogPrimitive.Portal
11 |
12 | const DialogClose = DialogPrimitive.Close
13 |
14 | const DialogOverlay = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
25 |
26 | const DialogContent = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef & {
29 | onOverlayClick?: () => void
30 | isSettings?: boolean
31 | isAlert?: boolean
32 | }
33 | >(({ className, children, isAlert, isSettings, onOverlayClick, ...props }, ref) => (
34 |
35 |
39 |
52 | {children}
53 | {!isAlert && (
54 |
60 |
61 | Close
62 |
63 | )}
64 |
65 |
66 | ))
67 | DialogContent.displayName = DialogPrimitive.Content.displayName
68 |
69 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
70 |
71 | )
72 | DialogHeader.displayName = 'DialogHeader'
73 |
74 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
75 |
82 | )
83 | DialogFooter.displayName = 'DialogFooter'
84 |
85 | const DialogTitle = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
94 | ))
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogTrigger,
114 | DialogClose,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription
120 | }
121 |
--------------------------------------------------------------------------------
/src/renderer/src/components/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as DialogPrimitive from '@radix-ui/react-dialog'
3 |
4 | import { cn } from '@renderer/lib/cn'
5 |
6 | const Dialog = DialogPrimitive.Root
7 |
8 | const DialogTrigger = DialogPrimitive.Trigger
9 |
10 | const DialogPortal = DialogPrimitive.Portal
11 |
12 | const DialogClose = DialogPrimitive.Close
13 |
14 | const DialogOverlay = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
25 |
26 | const DialogContent = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef & {
29 | onOverlayClick?: () => void
30 | isSettings?: boolean
31 | isAlert?: boolean
32 | }
33 | >(({ className, children, isAlert, isSettings, onOverlayClick, ...props }, ref) => (
34 |
35 |
39 |
52 | {children}
53 | {!isAlert && (
54 |
60 |
61 | Close
62 |
63 | )}
64 |
65 |
66 | ))
67 | DialogContent.displayName = DialogPrimitive.Content.displayName
68 |
69 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
70 |
71 | )
72 | DialogHeader.displayName = 'DialogHeader'
73 |
74 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
75 |
82 | )
83 | DialogFooter.displayName = 'DialogFooter'
84 |
85 | const DialogTitle = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
94 | ))
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogTrigger,
114 | DialogClose,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription
120 | }
121 |
--------------------------------------------------------------------------------
/src/renderer/src/pages/updater.tsx:
--------------------------------------------------------------------------------
1 | import { ProgressInfo } from 'electron-updater'
2 | import { useEffect, useState } from 'react'
3 | import prettyBytes from 'pretty-bytes'
4 | import { Button } from '@renderer/components/button'
5 | import { actionsProxy } from '@renderer/lib/actions-proxy'
6 |
7 | export function Component() {
8 | const updateInfoQuery = actionsProxy.getUpdateInfo.useQuery()
9 |
10 | const [progressInfo, setProgressInfo] = useState(null)
11 | const [status, setStatus] = useState(null)
12 |
13 | const updateInfo = updateInfoQuery.data
14 |
15 | useEffect(() => {
16 | const unlisten = window.electron.ipcRenderer.on(
17 | 'download-progress',
18 | (_, info: ProgressInfo) => {
19 | setProgressInfo(info)
20 | }
21 | )
22 |
23 | return unlisten
24 | }, [])
25 |
26 | useEffect(() => {
27 | const unlisten = window.electron.ipcRenderer.on('update-downloaded', () => {
28 | setStatus('downloaded')
29 | })
30 |
31 | return unlisten
32 | }, [])
33 |
34 | if (!updateInfo) return null
35 |
36 | return (
37 | <>
38 |
39 |
40 |
41 | App Update
42 |
43 |
44 | {status ? (
45 | <>
46 |
47 |
48 | {status === 'downloaded'
49 | ? `New version of ${APP_NAME} is ready to install`
50 | : `Downloading ${APP_NAME}...`}
51 |
52 |
53 | {progressInfo && status === 'downloading' && (
54 | {prettyBytes(progressInfo.bytesPerSecond)}/s
55 | )}
56 |
57 |
65 | >
66 | ) : (
67 | <>
68 | New version of {APP_NAME} is available
69 |
70 | {APP_NAME} {updateInfo?.version} is now available — you have {APP_VERSION}. Would
71 | you like to update now?
72 |
73 | >
74 | )}
75 |
76 | {status === 'downloaded' ? (
77 |
85 | ) : (
86 |
96 | )}
97 |
105 |
106 |
107 |
108 |
109 | {Array.isArray(updateInfo?.releaseNotes) && (
110 |
111 | {updateInfo.releaseNotes.map((note: any) => {
112 | return (
113 |
114 | v{note.version}
115 |
119 |
120 | )
121 | })}
122 |
123 | )}
124 |
125 |
126 | >
127 | )
128 | }
129 |
--------------------------------------------------------------------------------
/src/renderer/src/components/settings-dialog.tsx:
--------------------------------------------------------------------------------
1 | import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './dialog'
2 | import { saveConfig, useConfig } from '@renderer/lib/config'
3 | import { Control } from './control'
4 | import { Input } from './input'
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuItem,
9 | DropdownMenuTrigger
10 | } from './dropdown'
11 | import { models } from '@shared/constants'
12 |
13 | export const SettingsDialog = ({ children }: { children: React.ReactNode }) => {
14 | const configQuery = useConfig()
15 |
16 | const modelId = configQuery.data?.model ?? 'gpt-3.5-turbo'
17 | const model = models.find((m) => m.value === modelId)
18 |
19 | return (
20 |
124 | )
125 | }
126 |
--------------------------------------------------------------------------------
/src/renderer/src/pages/connections/[id]/index.tsx:
--------------------------------------------------------------------------------
1 | import { SidebarFooter } from '@renderer/components/sidebar'
2 | import { ConnectionIcon, ConnectionsMenu } from '@renderer/components/connections-menu'
3 | import { DropdownMenu, DropdownMenuTrigger } from '@renderer/components/dropdown'
4 | import { SidebarSection } from '@renderer/components/sidebar-section'
5 | import { actionsProxy } from '@renderer/lib/actions-proxy'
6 | import { useConnections, useSavedQueries } from '@renderer/lib/store'
7 | import { genId } from '@renderer/lib/utils'
8 | import { Link, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom'
9 | import { cn } from '@renderer/lib/cn'
10 |
11 | export const Component = () => {
12 | const location = useLocation()
13 | const params = useParams<{ id: string; queryId: string }>()
14 | const connectionId = params.id!
15 |
16 | const connectionsQuery = useConnections()
17 | const navigate = useNavigate()
18 |
19 | const connection = connectionsQuery.data?.find((c) => c.id === connectionId)
20 |
21 | const navItems = [
22 | {
23 | label: 'Schema',
24 | href: `/connections/${connectionId}/schema`
25 | }
26 | ]
27 |
28 | const queries = useSavedQueries(connectionId)
29 |
30 | const createQuery = actionsProxy.createQuery.useMutation({
31 | onSuccess(_, variables) {
32 | navigate(`/connections/${connectionId}/queries/${variables.id}`)
33 | queries.refetch()
34 | }
35 | })
36 |
37 | if (!connection) return null
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
57 |
58 |
62 |
63 |
64 |
65 |
66 |
67 | {navItems.map((item) => {
68 | const isActive = location.pathname === item.href
69 | return (
70 |
78 | {item.label}
79 |
80 | )
81 | })}
82 |
83 |
84 |
85 |
93 | createQuery.mutate({
94 | id: genId(),
95 | createdAt: new Date(),
96 | title: 'New Query',
97 | connectionId: connectionId,
98 | query: ''
99 | })
100 | }
101 | >
102 |
103 |
104 | }
105 | >
106 |
107 | {queries.data?.map((query) => {
108 | const isActive = params.queryId === query.id
109 | return (
110 |
118 | {query.title}
119 |
120 | )
121 | })}
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | )
132 | }
133 |
--------------------------------------------------------------------------------
/src/renderer/src/components/data-table.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import {
4 | ColumnDef,
5 | flexRender,
6 | getCoreRowModel,
7 | useReactTable,
8 | getPaginationRowModel
9 | } from '@tanstack/react-table'
10 |
11 | import {
12 | Table,
13 | TableBody,
14 | TableCell,
15 | TableHead,
16 | TableHeader,
17 | TableRow
18 | } from '@renderer/components/table'
19 | import { Button } from './button'
20 | import { useState } from 'react'
21 | import { Popover, PopoverTrigger, PopoverContent } from './popover'
22 | import { Input } from './input'
23 |
24 | interface DataTableProps {
25 | columns: ColumnDef[]
26 | data: TData[]
27 | }
28 |
29 | const PAGE_SIZE = 100
30 |
31 | export const DataTable = ({ columns, data }: DataTableProps) => {
32 | const [pagination, setPagination] = useState({
33 | pageIndex: 0, //initial page index
34 | pageSize: PAGE_SIZE
35 | })
36 |
37 | const moreThanOnePage = data.length > PAGE_SIZE
38 |
39 | const table = useReactTable({
40 | data,
41 | columns,
42 | getPaginationRowModel: getPaginationRowModel(),
43 | getCoreRowModel: getCoreRowModel(),
44 | onPaginationChange: setPagination, //update the pagination state when internal APIs mutate the pagination state
45 | state: {
46 | //...
47 | pagination
48 | }
49 | })
50 |
51 | const [showPageSettings, setShowPageSettings] = useState(false)
52 |
53 | return (
54 | <>
55 |
56 |
57 | {table.getHeaderGroups().map((headerGroup) => (
58 |
59 | {headerGroup.headers.map((header) => {
60 | return (
61 |
62 | {header.isPlaceholder
63 | ? null
64 | : flexRender(header.column.columnDef.header, header.getContext())}
65 |
66 | )
67 | })}
68 |
69 | ))}
70 |
71 |
72 | {table.getRowModel().rows?.length ? (
73 | table.getRowModel().rows.map((row) => (
74 |
75 | {row.getVisibleCells().map((cell) => (
76 |
77 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
78 |
79 | ))}
80 |
81 | ))
82 | ) : (
83 |
84 |
85 | No results.
86 |
87 |
88 | )}
89 |
90 |
91 | {moreThanOnePage && (
92 |
93 |
102 |
103 |
104 |
105 |
108 |
109 |
110 |
141 |
142 |
143 |
144 |
153 |
154 | )}
155 | >
156 | )
157 | }
158 |
--------------------------------------------------------------------------------
/src/main/actions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Menu,
3 | MessageBoxOptions,
4 | OpenDialogOptions,
5 | SaveDialogOptions,
6 | clipboard,
7 | dialog
8 | } from 'electron'
9 | import { loadConfig, saveConfig } from './config'
10 | import { Config, Connection, Query } from '@shared/types'
11 | import { connectDatabase, disconnectDatabase, getDatabaseSchema, queryDatabase } from './connection'
12 | import { appDB, appSchema } from './app-db'
13 | import { desc, eq } from 'drizzle-orm'
14 | import { checkForUpdates, downloadUpdate, getUpdateInfo, quitAndInstall } from './updater'
15 | import { WindowId, showUpdaterWindow, windows } from './windows'
16 |
17 | type ActionContext = {
18 | sender: Electron.WebContents
19 | }
20 |
21 | type ActionFunction = (args: {
22 | context: ActionContext
23 | input: TInput
24 | }) => Promise
25 |
26 | const createChainFns = () => {
27 | return {
28 | action: (action: ActionFunction) => {
29 | return (context: ActionContext, input: TInput) => action({ context, input })
30 | }
31 | }
32 | }
33 |
34 | const createChain = () => {
35 | return {
36 | input() {
37 | return createChainFns()
38 | },
39 |
40 | ...createChainFns()
41 | }
42 | }
43 |
44 | const chain = createChain()
45 |
46 | export const actions = {
47 | getUpdateInfo: chain.action(async () => {
48 | return getUpdateInfo()
49 | }),
50 |
51 | checkForUpdates: chain.action(async () => {
52 | return checkForUpdates()
53 | }),
54 |
55 | downloadUpdate: chain.action(async () => {
56 | return downloadUpdate()
57 | }),
58 |
59 | quitAndInstall: chain.action(async () => {
60 | return quitAndInstall()
61 | }),
62 |
63 | closeWindow: chain.input<{ id: WindowId }>().action(async ({ input }) => {
64 | windows.get(input.id)?.close()
65 | }),
66 |
67 | showUpdaterWindow: chain.action(async () => {
68 | showUpdaterWindow()
69 | }),
70 |
71 | inspectElement: chain.input<{ x: number; y: number }>().action(async ({ input, context }) => {
72 | context.sender.inspectElement(input.x, input.y)
73 | }),
74 |
75 | showContextMenu: chain
76 | .input<{ items: Array<{ type: 'text'; label: string } | { type: 'separator' }> }>()
77 | .action(async ({ input, context }) => {
78 | const menu = Menu.buildFromTemplate(
79 | input.items.map((item, index) => {
80 | if (item.type === 'separator') {
81 | return {
82 | type: 'separator' as const
83 | }
84 | }
85 | return {
86 | label: item.label,
87 | click() {
88 | context.sender.send('menu-click', index)
89 | }
90 | }
91 | })
92 | )
93 |
94 | menu.popup({
95 | callback: () => {
96 | context.sender.send('menu-closed')
97 | }
98 | })
99 | }),
100 |
101 | loadConfig: chain.action(() => {
102 | return loadConfig()
103 | }),
104 |
105 | saveConfig: chain.input<{ config: Config }>().action(({ input }) => {
106 | return saveConfig(input.config)
107 | }),
108 |
109 | showOpenDialog: chain.input<{ options: OpenDialogOptions }>().action(async ({ input }) => {
110 | const result = await dialog.showOpenDialog(input.options)
111 | return result.filePaths
112 | }),
113 |
114 | showSaveDialog: chain.input<{ options: SaveDialogOptions }>().action(async ({ input }) => {
115 | const result = await dialog.showSaveDialog(input.options)
116 | return result.filePath
117 | }),
118 |
119 | showConfirmDialog: chain
120 | .input<{ title: string; message: string; options?: MessageBoxOptions }>()
121 | .action(async ({ input }) => {
122 | const result = await dialog.showMessageBox({
123 | title: input.title,
124 | message: input.message,
125 | buttons: ['Yes', 'No'],
126 | ...input.options
127 | })
128 | return result.response === 0
129 | }),
130 |
131 | connectDatabase: chain.input<{ connectionId: string }>().action(async ({ input }) => {
132 | return connectDatabase(input.connectionId)
133 | }),
134 |
135 | queryDatabase: chain.input[0]>().action(async ({ input }) => {
136 | return queryDatabase(input)
137 | }),
138 |
139 | getDatabaseSchema: chain
140 | .input[0]>()
141 | .action(async ({ input }) => {
142 | return getDatabaseSchema(input)
143 | }),
144 |
145 | getConnections: chain.action(async () => {
146 | return appDB.query.connection.findMany({
147 | orderBy: desc(appSchema.connection.createdAt)
148 | })
149 | }),
150 |
151 | updateConnection: chain
152 | .input & { id: string }>()
153 | .action(async ({ input }) => {
154 | await disconnectDatabase(input.id)
155 |
156 | await appDB
157 | .update(appSchema.connection)
158 | .set(input)
159 | .where(eq(appSchema.connection.id, input.id))
160 |
161 | return input
162 | }),
163 |
164 | deleteConnection: chain.input<{ id: string }>().action(async ({ input }) => {
165 | await disconnectDatabase(input.id)
166 |
167 | await appDB.delete(appSchema.connection).where(eq(appSchema.connection.id, input.id))
168 | }),
169 |
170 | createConnection: chain.input().action(async ({ input }) => {
171 | await appDB.insert(appSchema.connection).values(input)
172 | return input
173 | }),
174 |
175 | getQueries: chain.input<{ connectionId: string }>().action(async ({ input }) => {
176 | return appDB.query.query.findMany({
177 | orderBy: desc(appSchema.query.createdAt),
178 | where: eq(appSchema.query.connectionId, input.connectionId)
179 | })
180 | }),
181 |
182 | updateQuery: chain.input & { id: string }>().action(async ({ input }) => {
183 | await appDB.update(appSchema.query).set(input).where(eq(appSchema.query.id, input.id))
184 | }),
185 |
186 | deleteQuery: chain.input<{ id: string }>().action(async ({ input }) => {
187 | await appDB.delete(appSchema.query).where(eq(appSchema.query.id, input.id))
188 | }),
189 |
190 | createQuery: chain.input().action(async ({ input }) => {
191 | await appDB.insert(appSchema.query).values(input)
192 | }),
193 |
194 | copyToClipboard: chain.input<{ text: string }>().action(async ({ input }) => {
195 | clipboard.writeText(input.text)
196 | })
197 | }
198 |
--------------------------------------------------------------------------------
/src/main/connection.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import { Connection, DatabaseColumn, DatabaseSchema, QueryDatabaseResult } from '@shared/types'
3 | import { identify } from 'sql-query-identifier'
4 | import { appDB, appSchema } from './app-db'
5 | import { eq } from 'drizzle-orm'
6 |
7 | type DatabaseInstance = {
8 | execute = Record>(
9 | query: string,
10 | variables?: any[]
11 | ): Promise<{
12 | rows: T[]
13 | rowsAffected?: number | null
14 | }>
15 |
16 | close(): Promise
17 | }
18 | const instances = new Map()
19 |
20 | export const disconnectDatabase = async (connectionId: string) => {
21 | const instance = instances.get(connectionId)
22 | if (instance) {
23 | await instance.close()
24 | instances.delete(connectionId)
25 | }
26 | }
27 |
28 | export const connectDatabase = async (connectionId: string, disabledSSL?: boolean) => {
29 | const connection = await appDB.query.connection.findFirst({
30 | where: eq(appSchema.connection.id, connectionId)
31 | })
32 |
33 | if (!connection) {
34 | return null
35 | }
36 |
37 | if (instances.has(connection.id)) {
38 | return connection
39 | }
40 |
41 | let db: DatabaseInstance | undefined
42 |
43 | if (connection.type === 'postgresql') {
44 | const pg = await import('pg')
45 | const client = new pg.default.Client({
46 | host: connection.host || '127.0.0.1',
47 | port: connection.port ? Number(connection.port) : 5432,
48 | user: connection.user ?? '',
49 | password: connection.password ?? '',
50 | database: connection.database,
51 | ssl: disabledSSL
52 | ? false
53 | : {
54 | rejectUnauthorized: false,
55 | ca: fs.readFileSync('/etc/ssl/cert.pem')
56 | }
57 | })
58 |
59 | try {
60 | await client.connect()
61 | } catch (error) {
62 | if (
63 | error instanceof Error &&
64 | error.message.includes('The server does not support SSL connections')
65 | ) {
66 | return connectDatabase(connectionId, true)
67 | }
68 | throw error
69 | }
70 |
71 | client.on('end', () => {
72 | instances.delete(connection.id)
73 | })
74 |
75 | db = {
76 | async execute(query, variables) {
77 | const result = await client.query(query, variables)
78 | return { rows: result.rows, rowsAffected: result.rowCount }
79 | },
80 |
81 | async close() {
82 | await client.end()
83 | }
84 | }
85 | } else if (connection.type === 'mysql') {
86 | const mysql = await import('mysql2/promise')
87 | const client = mysql.default.createPool({
88 | host: connection.host || '127.0.0.1',
89 | port: connection.port ? Number(connection.port) : 3306,
90 | user: connection.user ?? '',
91 | password: connection.password ?? '',
92 | database: connection.database,
93 | ssl: {
94 | ca: fs.readFileSync('/etc/ssl/cert.pem'),
95 | rejectUnauthorized: false
96 | },
97 | enableKeepAlive: true,
98 | connectionLimit: 3
99 | })
100 |
101 | db = {
102 | async execute(query, variables) {
103 | const [rows] = await client.execute(query, variables)
104 |
105 | return { rows: rows as any, rowsAffected: 0 }
106 | },
107 |
108 | async close() {
109 | await client.end()
110 | }
111 | }
112 | } else {
113 | const { createClient } = await import('@libsql/client')
114 | const client = createClient({
115 | url: `file:${connection.database}`
116 | })
117 |
118 | db = {
119 | async execute(query, variables) {
120 | const result = await client.execute({
121 | sql: query,
122 | args: variables || []
123 | })
124 |
125 | return {
126 | rows: result.rows as any,
127 | rowsAffected: result.rowsAffected
128 | }
129 | },
130 |
131 | async close() {
132 | instances.delete(connection.id)
133 | client.close()
134 | }
135 | }
136 | }
137 |
138 | instances.set(connection.id, db)
139 |
140 | return connection
141 | }
142 |
143 | export const queryDatabase = async ({
144 | connectionId,
145 | query
146 | }: {
147 | connectionId: string
148 | query: string
149 | }) => {
150 | const db = instances.get(connectionId)
151 |
152 | const statements = identify(query)
153 |
154 | const results: QueryDatabaseResult[] = statements.map((statement) => {
155 | return { statement, rows: [] }
156 | })
157 |
158 | let hasError = false
159 | for (const result of results) {
160 | if (hasError) {
161 | result.aborted = true
162 | continue
163 | }
164 | try {
165 | if (!db) {
166 | throw new Error('No active connection to the database')
167 | }
168 |
169 | const dbResult = await db.execute(result.statement.text)
170 | result.rows = dbResult.rows
171 | result.rowsAffected = dbResult.rowsAffected
172 | } catch (error) {
173 | hasError = true
174 | console.error(error)
175 | result.error =
176 | error instanceof Error ? error.message : typeof error === 'string' ? error : 'Unknown error'
177 | }
178 | }
179 |
180 | return results
181 | }
182 |
183 | export const getDatabaseSchema = async ({
184 | connection
185 | }: {
186 | connection: Connection
187 | }): Promise => {
188 | const db = instances.get(connection.id)
189 |
190 | if (!db) {
191 | throw new Error('Database not found')
192 | }
193 |
194 | const tables = await db
195 | .execute<{
196 | table_name: string
197 | }>(
198 | connection.type === 'postgresql'
199 | ? `SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE' AND table_schema = 'public';`
200 | : connection.type === 'mysql'
201 | ? `SELECT table_name as table_name FROM information_schema.tables WHERE table_schema = '${connection.database}';`
202 | : `SELECT name as table_name FROM sqlite_master WHERE type = 'table';`
203 | )
204 | .then((res) => res.rows)
205 |
206 | const tablesWithColumns = await Promise.all(
207 | tables.map(async (table) => {
208 | const columns = await db
209 | .execute<{
210 | column_name: string
211 | data_type: string
212 | is_nullable?: string
213 | notnull?: number
214 | column_default: string | null
215 | }>(
216 | connection.type === 'postgresql'
217 | ? `SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = '${table.table_name}';`
218 | : connection.type === 'mysql'
219 | ? `SELECT column_name as column_name, data_type as data_type, is_nullable as is_nullable, column_default as column_default FROM information_schema.columns WHERE table_name = '${table.table_name}' AND table_schema = '${connection.database}';`
220 | : `SELECT name as column_name, type as data_type, "notnull", dflt_value as column_default FROM pragma_table_info('${table.table_name}');`
221 | )
222 | .then((res) => {
223 | return res.rows.map((row) => {
224 | return {
225 | name: row.column_name,
226 | type: row.data_type,
227 | nullable:
228 | typeof row.notnull === 'number' ? row.notnull === 0 : row.is_nullable === 'YES',
229 | default: row.column_default
230 | } satisfies DatabaseColumn
231 | })
232 | })
233 | return { name: table.table_name, columns }
234 | })
235 | )
236 |
237 | return {
238 | tables: tablesWithColumns
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/src/renderer/src/components/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
3 |
4 | import { cn } from '@renderer/lib/cn'
5 |
6 | const DropdownMenu = DropdownMenuPrimitive.Root
7 |
8 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
9 |
10 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
11 |
12 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
13 |
14 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
15 |
16 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
17 |
18 | const DropdownMenuSubTrigger = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef & {
21 | inset?: boolean
22 | }
23 | >(({ className, inset, children, ...props }, ref) => (
24 |
33 | {children}
34 |
35 |
36 | ))
37 | DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
38 |
39 | const DropdownMenuSubContent = React.forwardRef<
40 | React.ElementRef,
41 | React.ComponentPropsWithoutRef
42 | >(({ className, ...props }, ref) => (
43 |
51 | ))
52 | DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
53 |
54 | const DropdownMenuContent = React.forwardRef<
55 | React.ElementRef,
56 | Omit, 'prefix'> & {
57 | prefix?: React.ReactNode
58 | suffix?: React.ReactNode
59 | matchTriggerWidth?: boolean | 'min'
60 | }
61 | >(({ className, sideOffset = 4, prefix, suffix, matchTriggerWidth, children, ...props }, ref) => (
62 |
63 |
75 | {prefix}
76 | {children}
77 | {suffix}
78 |
79 |
80 | ))
81 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
82 |
83 | const DropdownMenuItem = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef & {
86 | inset?: boolean
87 | }
88 | >(({ className, inset, ...props }, ref) => (
89 |
98 | ))
99 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
100 |
101 | const DropdownMenuCheckboxItem = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, children, checked, ...props }, ref) => (
105 |
114 |
115 |
116 |
117 |
118 |
119 | {children}
120 |
121 | ))
122 | DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
157 | ))
158 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
159 |
160 | const DropdownMenuSeparator = React.forwardRef<
161 | React.ElementRef,
162 | React.ComponentPropsWithoutRef
163 | >(({ className, ...props }, ref) => (
164 |
169 | ))
170 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
171 |
172 | const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {
173 | return
174 | }
175 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
176 |
177 | export {
178 | DropdownMenu,
179 | DropdownMenuTrigger,
180 | DropdownMenuContent,
181 | DropdownMenuItem,
182 | DropdownMenuCheckboxItem,
183 | DropdownMenuRadioItem,
184 | DropdownMenuLabel,
185 | DropdownMenuSeparator,
186 | DropdownMenuShortcut,
187 | DropdownMenuGroup,
188 | DropdownMenuPortal,
189 | DropdownMenuSub,
190 | DropdownMenuSubContent,
191 | DropdownMenuSubTrigger,
192 | DropdownMenuRadioGroup
193 | }
194 |
--------------------------------------------------------------------------------
/src/renderer/src/components/dropdown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
3 |
4 | import { cn } from '@renderer/lib/cn'
5 |
6 | const DropdownMenu = DropdownMenuPrimitive.Root
7 |
8 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
9 |
10 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
11 |
12 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
13 |
14 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
15 |
16 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
17 |
18 | const DropdownMenuSubTrigger = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef & {
21 | inset?: boolean
22 | }
23 | >(({ className, inset, children, ...props }, ref) => (
24 |
33 | {children}
34 |
35 |
36 | ))
37 | DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
38 |
39 | const DropdownMenuSubContent = React.forwardRef<
40 | React.ElementRef,
41 | React.ComponentPropsWithoutRef
42 | >(({ className, ...props }, ref) => (
43 |
51 | ))
52 | DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
53 |
54 | const DropdownMenuContent = React.forwardRef<
55 | React.ElementRef,
56 | Omit, 'prefix'> & {
57 | prefix?: React.ReactNode
58 | suffix?: React.ReactNode
59 | matchTriggerWidth?: boolean | 'min'
60 | }
61 | >(({ className, sideOffset = 4, prefix, suffix, matchTriggerWidth, children, ...props }, ref) => (
62 |
63 |
75 | {prefix}
76 | {children}
77 | {suffix}
78 |
79 |
80 | ))
81 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
82 |
83 | const DropdownMenuItem = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef & {
86 | inset?: boolean
87 | }
88 | >(({ className, inset, ...props }, ref) => (
89 |
98 | ))
99 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
100 |
101 | const DropdownMenuCheckboxItem = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, children, checked, ...props }, ref) => (
105 |
114 |
115 |
116 |
117 |
118 |
119 | {children}
120 |
121 | ))
122 | DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
123 |
124 | const DropdownMenuRadioItem = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, children, ...props }, ref) => (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | ))
144 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
145 |
146 | const DropdownMenuLabel = React.forwardRef<
147 | React.ElementRef,
148 | React.ComponentPropsWithoutRef & {
149 | inset?: boolean
150 | }
151 | >(({ className, inset, ...props }, ref) => (
152 |
157 | ))
158 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
159 |
160 | const DropdownMenuSeparator = React.forwardRef<
161 | React.ElementRef,
162 | React.ComponentPropsWithoutRef
163 | >(({ className, ...props }, ref) => (
164 |
169 | ))
170 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
171 |
172 | const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {
173 | return
174 | }
175 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
176 |
177 | export {
178 | DropdownMenu,
179 | DropdownMenuTrigger,
180 | DropdownMenuContent,
181 | DropdownMenuItem,
182 | DropdownMenuCheckboxItem,
183 | DropdownMenuRadioItem,
184 | DropdownMenuLabel,
185 | DropdownMenuSeparator,
186 | DropdownMenuShortcut,
187 | DropdownMenuGroup,
188 | DropdownMenuPortal,
189 | DropdownMenuSub,
190 | DropdownMenuSubContent,
191 | DropdownMenuSubTrigger,
192 | DropdownMenuRadioGroup
193 | }
194 |
--------------------------------------------------------------------------------
/src/renderer/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@renderer/components/button'
2 | import { ConnectionIcon } from '@renderer/components/connections-menu'
3 | import { Control } from '@renderer/components/control'
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuTrigger
9 | } from '@renderer/components/dropdown'
10 | import { Input } from '@renderer/components/input'
11 | import { SidebarFooter } from '@renderer/components/sidebar'
12 | import { SidebarSection } from '@renderer/components/sidebar-section'
13 | import { actionsProxy } from '@renderer/lib/actions-proxy'
14 | import { cn } from '@renderer/lib/cn'
15 | import {
16 | connectionTypes,
17 | formatConnectionType,
18 | getConnectionDefaultValues
19 | } from '@renderer/lib/database'
20 | import { showNativeMenu } from '@renderer/lib/native-menu'
21 | import { useConnections } from '@renderer/lib/store'
22 | import { genId } from '@renderer/lib/utils'
23 | import { Connection, ConnectionType } from '@shared/types'
24 | import { useRef } from 'react'
25 | import { Link, useNavigate, useSearchParams } from 'react-router-dom'
26 | import Url from 'url-parse'
27 |
28 | export function Component() {
29 | const [searchParams, setSearchParams] = useSearchParams()
30 | const connectionsQuery = useConnections()
31 | const connectionId = searchParams.get('id')
32 |
33 | const connection = connectionsQuery.data?.find((c) => c.id === connectionId)
34 | const connectionsCount = connectionsQuery.data?.length || 0
35 |
36 | const navigate = useNavigate()
37 |
38 | const createConnection = actionsProxy.createConnection.useMutation({
39 | onSuccess(_, variables) {
40 | connectionsQuery.refetch()
41 | setSearchParams({ id: variables.id })
42 | }
43 | })
44 |
45 | const deleteConnection = actionsProxy.deleteConnection.useMutation({
46 | onSuccess() {
47 | connectionsQuery.refetch()
48 | }
49 | })
50 |
51 | return (
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
63 |
64 |
65 | {connectionTypes.map((type) => (
66 | {
70 | createConnection.mutate({
71 | id: genId(),
72 | type,
73 | createdAt: new Date(),
74 | nickname: `Untitled Connection ${connectionsCount + 1}`,
75 | ...getConnectionDefaultValues()
76 | })
77 | }}
78 | >
79 |
80 | {formatConnectionType(type)}
81 |
82 | ))}
83 |
84 |
85 |
86 |
87 | {connectionsCount > 0 ? (
88 |
89 | {connectionsQuery.data?.map((connection) => {
90 | const isActive = connection.id === connectionId
91 | return (
92 | {
100 | if (e.detail === 2) {
101 | e.preventDefault()
102 |
103 | navigate(`/connections/${connection.id}/schema`)
104 | }
105 | }}
106 | onContextMenu={(e) => {
107 | e.preventDefault()
108 | e.stopPropagation()
109 |
110 | showNativeMenu(
111 | [
112 | {
113 | type: 'text' as const,
114 | label: 'Delete',
115 | click: async () => {
116 | if (
117 | await actionsProxy.showConfirmDialog.invoke({
118 | title: 'Delete Connection',
119 | message: `Are you sure you want to delete this connection?`
120 | })
121 | ) {
122 | deleteConnection.mutate({ id: connection.id })
123 | }
124 | }
125 | }
126 | ],
127 | e
128 | )
129 | }}
130 | >
131 |
132 | {connection.nickname || 'Untitled'}
133 |
134 | )
135 | })}
136 |
137 | ) : (
138 |
139 | )}
140 |
141 |
142 |
143 |
144 | {connection ? (
145 |
146 |
147 |
148 | ) : (
149 |
150 |
151 | QueryBase is in beta, do not use it with critically important databases.
152 |
153 |
154 | )}
155 |
156 | )
157 | }
158 |
159 | const DatabaseSettings = ({
160 | connection,
161 | type
162 | }: {
163 | connection: Connection
164 | type: ConnectionType
165 | }) => {
166 | const navigate = useNavigate()
167 | const formRef = useRef(null)
168 | const hostInputRef = useRef(null)
169 | const portInputRef = useRef(null)
170 | const userInputRef = useRef(null)
171 | const passwordInputRef = useRef(null)
172 | const databaseInputRef = useRef(null)
173 | const nicknameInputRef = useRef(null)
174 |
175 | const updateConnection = actionsProxy.updateConnection.useMutation({
176 | onMutate(variables) {
177 | actionsProxy.getConnections.setQueryData(undefined, (prev) => {
178 | if (!prev) return prev
179 |
180 | return prev.map((c) => {
181 | if (c.id === variables.id) {
182 | return {
183 | ...c,
184 | ...variables
185 | }
186 | }
187 | return c
188 | })
189 | })
190 | }
191 | })
192 |
193 | const handlePaste = (e: React.ClipboardEvent) => {
194 | const form = formRef.current
195 | if (connection.type === 'sqlite' || !form) return
196 |
197 | const text = e.clipboardData.getData('text/plain')
198 | try {
199 | const url = new Url(text)
200 |
201 | const { hostname, port, username, password, pathname, protocol, query } = url
202 |
203 | if (connection.type === 'mysql' && protocol !== 'mysql:') {
204 | return
205 | }
206 |
207 | if (connection.type === 'postgresql' && !['postgresql:', 'postgres:'].includes(protocol)) {
208 | return
209 | }
210 |
211 | e.preventDefault()
212 |
213 | const database = pathname.replace(/^\//, '')
214 |
215 | if (hostInputRef.current) {
216 | hostInputRef.current.value = hostname
217 | updateConnection.mutate({
218 | id: connection.id,
219 | host: hostname
220 | })
221 | }
222 | if (portInputRef.current) {
223 | portInputRef.current.value = port
224 | updateConnection.mutate({
225 | id: connection.id,
226 | port: port
227 | })
228 | }
229 | if (userInputRef.current) {
230 | userInputRef.current.value = username
231 | updateConnection.mutate({
232 | id: connection.id,
233 | user: username
234 | })
235 | }
236 | if (passwordInputRef.current) {
237 | passwordInputRef.current.value = password
238 | updateConnection.mutate({
239 | id: connection.id,
240 | password: password
241 | })
242 | }
243 | if (databaseInputRef.current) {
244 | databaseInputRef.current.value = database
245 | updateConnection.mutate({
246 | id: connection.id,
247 | database: database
248 | })
249 | }
250 | } catch {}
251 | }
252 |
253 | return (
254 | <>
255 |
263 |
264 |
265 | {connection.nickname}
266 |
267 |
268 |
436 | >
437 | )
438 | }
439 |
--------------------------------------------------------------------------------
/src/renderer/src/pages/connections/[id]/queries/[queryId].tsx:
--------------------------------------------------------------------------------
1 | import { useCodeMirror } from '@uiw/react-codemirror'
2 | import { sql as sqlExtension } from '@codemirror/lang-sql'
3 | import { EditorView } from '@codemirror/view'
4 | import { useEffect, useRef, useState } from 'react'
5 | import { useParams } from 'react-router-dom'
6 | import { actionsProxy } from '@renderer/lib/actions-proxy'
7 | import { databaseSchemaToSQL } from '@renderer/lib/database'
8 | import AutosizeInput from 'react-input-autosize'
9 | import { useConnections, useSavedQueries, useSchema } from '@renderer/lib/store'
10 | import { Button } from '@renderer/components/button'
11 | import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/popover'
12 | import { Control } from '@renderer/components/control'
13 | import { Textarea } from '@renderer/components/input'
14 | import { ConnectionType, QueryDatabaseResult } from '@shared/types'
15 | import Markdown from 'react-markdown'
16 | import { UITooltip } from '@renderer/components/ui-tooltip'
17 | import { highlightNearestStatementExtension } from '@renderer/lib/codemirror'
18 | import { formatError } from '@renderer/lib/utils'
19 | import { useMutation } from '@tanstack/react-query'
20 | import { generateFixForError, generateSQL } from '@renderer/lib/openai'
21 | import { useConfig } from '@renderer/lib/config'
22 | import { cn } from '@renderer/lib/cn'
23 | import { DataTable } from '@renderer/components/data-table'
24 | import { ColumnDef } from '@tanstack/react-table'
25 |
26 | const editorTheme = EditorView.theme({
27 | '&': {
28 | height: '100%'
29 | },
30 | '.cm-scroller': {
31 | fontFamily: 'var(--font-mono)',
32 | fontSize: '15px'
33 | },
34 | '&.cm-focused': {
35 | outline: 'none'
36 | },
37 | '.cm-gutters,.cm-activeLineGutter': {
38 | backgroundColor: 'transparent',
39 | color: '#ccc'
40 | },
41 | '.cm-gutters': {
42 | borderRight: '0px'
43 | },
44 | '.cm-activeLineGutter': {
45 | color: '#000'
46 | }
47 | })
48 |
49 | export function Component() {
50 | const params = useParams<{ id: string; queryId: string }>()
51 | const connectionId = params.id!
52 | const queryId = params.queryId!
53 |
54 | const configQuery = useConfig()
55 | const config = configQuery.data || {}
56 | const connectionsQuery = useConnections()
57 | const queries = useSavedQueries(connectionId)
58 | const query = queries.data?.find((q) => q.id === queryId)
59 | const [isMagicPopoverOpen, setIsMagicPopoverOpen] = useState(false)
60 | const [selectedText, setSelectedText] = useState('')
61 | const [nearestQuery, setNearestQuery] = useState('')
62 |
63 | const connection = connectionsQuery.data?.find((c) => c.id === connectionId)
64 |
65 | const schemaQuery = useSchema(connectionId)
66 |
67 | const schema = schemaQuery.data
68 |
69 | const updateQuery = actionsProxy.updateQuery.useMutation({
70 | onMutate(variables) {
71 | actionsProxy.getQueries.setQueryData({ connectionId }, (prev) => {
72 | if (!prev) return prev
73 | return prev.map((item) => {
74 | if (item.id === queryId) {
75 | return {
76 | ...item,
77 | ...variables
78 | }
79 | }
80 | return item
81 | })
82 | })
83 | }
84 | })
85 |
86 | const editorElRef = useRef(null)
87 | const { setContainer, view: editorView } = useCodeMirror({
88 | value: query?.query ?? '',
89 | onChange(value) {
90 | updateQuery.mutate({ id: queryId, query: value })
91 | },
92 | basicSetup: {
93 | highlightActiveLine: false
94 | },
95 | onUpdate(viewUpdate) {
96 | // get selected text
97 | setSelectedText(
98 | viewUpdate.state.doc.sliceString(
99 | viewUpdate.state.selection.main.from,
100 | viewUpdate.state.selection.main.to
101 | )
102 | )
103 | },
104 | placeholder: 'Enter SQL here...',
105 | height: '100%',
106 | extensions: [
107 | EditorView.lineWrapping,
108 | editorTheme,
109 | highlightNearestStatementExtension({
110 | setNearestQuery
111 | }),
112 | sqlExtension({
113 | schema: schema?.tables.reduce((res, table) => {
114 | return {
115 | ...res,
116 | [table.name]: table.columns.map((col) => col.name)
117 | }
118 | }, {})
119 | })
120 | ]
121 | })
122 |
123 | const generateSQLMutation = useMutation({
124 | mutationFn: generateSQL,
125 | onError(error) {
126 | alert(error.message)
127 | },
128 | onSuccess(data) {
129 | if (data) {
130 | updateQuery.mutate({ id: queryId, query: data.content })
131 |
132 | if (editorView) {
133 | // insert the generated sql into the editor
134 | const { state } = editorView
135 | const { from } = state.selection.main
136 |
137 | // if active line is empty, insert the generated sql
138 | // otherwise, insert the generated sql in a new line
139 | const line = state.doc.lineAt(from)
140 | const isEmpty = line.text.trim() === ''
141 | let pos = line.from
142 | let content = data.content
143 | let selectionAnchor = pos
144 | let head = pos + content.length
145 |
146 | if (!isEmpty) {
147 | pos = line.to
148 | content = `\n${data.content}`
149 | selectionAnchor = pos + 1
150 | head = pos + content.length
151 | }
152 |
153 | setIsMagicPopoverOpen(false)
154 |
155 | editorView.dispatch({
156 | changes: {
157 | from: pos,
158 | to: pos,
159 | insert: content
160 | },
161 | selection: {
162 | anchor: selectionAnchor,
163 | head
164 | }
165 | })
166 | }
167 | }
168 | }
169 | })
170 |
171 | const [queryResponses, setQueryResponses] = useState<
172 | Record
173 | >({})
174 |
175 | const queryResponse = queryResponses[queryId]
176 |
177 | const clearQueryResponse = () => {
178 | setQueryResponses((prev) => {
179 | return {
180 | ...prev,
181 | [queryId]: null
182 | }
183 | })
184 | }
185 |
186 | const queryDatabase = actionsProxy.queryDatabase.useMutation({
187 | onSuccess(data) {
188 | setQueryResponses((prev) => ({
189 | ...prev,
190 | [queryId]: data
191 | }))
192 | }
193 | })
194 |
195 | useEffect(() => {
196 | if (editorElRef.current) {
197 | setContainer(editorElRef.current)
198 | }
199 | }, [])
200 |
201 | useEffect(() => {
202 | if (queryId && editorView) {
203 | if (import.meta.env.DEV) {
204 | // @ts-expect-error
205 | window.editorView = editorView
206 | }
207 | editorView.focus()
208 | }
209 | }, [queryId, editorView])
210 |
211 | const queryToRun = selectedText || nearestQuery
212 |
213 | if (!connection) return null
214 |
215 | return (
216 | <>
217 |
218 | {
224 | updateQuery.mutate({ id: queryId, title: e.target.value })
225 | }}
226 | />
227 |
228 |
229 |
230 |
231 |
234 |
235 | {
238 | e.preventDefault()
239 | editorView?.focus()
240 | }}
241 | >
242 |
283 |
284 |
285 |
298 |
299 |
300 | 0 ? 'shrink-0 h-editor-wrapper' : 'grow'
304 | )}
305 | >
306 |
307 |
308 |
309 | {queryResponse && queryResponse.length > 0 && (
310 |
315 | )}
316 | >
317 | )
318 | }
319 |
320 | const RenderQueryResponse = ({
321 | type,
322 | results,
323 | clearQueryResponse
324 | }: {
325 | type: ConnectionType
326 | results: QueryDatabaseResult[]
327 | clearQueryResponse: () => void
328 | }) => {
329 | const [activeIndex, setActiveIndex] = useState(0)
330 | const configQuery = useConfig()
331 |
332 | const result = results[activeIndex]
333 |
334 | const generateFixForErrorMutation = useMutation({
335 | mutationFn: generateFixForError,
336 | onError(error) {
337 | alert(error.message)
338 | }
339 | })
340 |
341 | if (!result) return null
342 |
343 | return (
344 |
345 |
346 |
347 | {results.map((result, index) => {
348 | const isActive = index === activeIndex
349 | return (
350 |
351 |
363 |
364 | )
365 | })}
366 |
367 |
368 |
375 |
376 | {result.error ? (
377 |
378 |
379 | {formatError(result.error)}
380 |
381 |
397 |
398 | {generateFixForErrorMutation.data && (
399 |
400 | {generateFixForErrorMutation.data.content}
401 |
402 | )}
403 |
404 |
405 | ) : result.aborted ? (
406 | Aborted
407 | ) : result.rows && result.rows.length > 0 ? (
408 |
409 |
410 |
411 | ) : typeof result.rowsAffected === 'number' ? (
412 |
413 |
414 | {result.rowsAffected} rows{' '}
415 | {result.statement.executionType === 'LISTING' ? 'returned' : 'affected'}
416 |
417 |
418 | ) : null}
419 |
420 | )
421 | }
422 |
423 | const RenderRows = ({ rows }: { rows: Record[] }) => {
424 | const columns: ColumnDef>[] = Object.keys(rows[0]).map((name) => {
425 | return {
426 | accessorKey: name,
427 | header: name,
428 | cell: ({ row }) => {
429 | const value = row.getValue(name)
430 | const valueStr =
431 | typeof value === 'string'
432 | ? value
433 | : value instanceof Date
434 | ? value.toISOString()
435 | : JSON.stringify(value)
436 | let copyTimeout: number | undefined
437 |
438 | return (
439 |
440 |
441 |
447 |
448 |
449 |
450 |
473 |
474 |
475 | {valueStr}
476 |
477 |
478 |
479 | )
480 | }
481 | }
482 | })
483 |
484 | return
485 | }
486 |
--------------------------------------------------------------------------------
| | |