diff --git a/.github/workflows/build-app-pro-beta.yaml b/.github/workflows/build-app-pro-beta.yaml index 0379a00fc..733dc36dd 100644 --- a/.github/workflows/build-app-pro-beta.yaml +++ b/.github/workflows/build-app-pro-beta.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: '${{ secrets.GH_TOKEN }}' path: dbgate-pro - ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 + ref: a2f824dc711b510a5e8235d3faf4aafab1965184 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-app-pro.yaml b/.github/workflows/build-app-pro.yaml index bf7615383..3ab892a19 100644 --- a/.github/workflows/build-app-pro.yaml +++ b/.github/workflows/build-app-pro.yaml @@ -39,7 +39,7 @@ jobs: repository: dbgate/dbgate-pro token: '${{ secrets.GH_TOKEN }}' path: dbgate-pro - ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 + ref: a2f824dc711b510a5e8235d3faf4aafab1965184 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-aws-pro.yaml b/.github/workflows/build-aws-pro.yaml index cecc55926..52fa8f321 100644 --- a/.github/workflows/build-aws-pro.yaml +++ b/.github/workflows/build-aws-pro.yaml @@ -36,7 +36,7 @@ jobs: repository: dbgate/dbgate-pro token: '${{ secrets.GH_TOKEN }}' path: dbgate-pro - ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 + ref: a2f824dc711b510a5e8235d3faf4aafab1965184 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-docker-pro.yaml b/.github/workflows/build-docker-pro.yaml index b650bde46..1da09a70a 100644 --- a/.github/workflows/build-docker-pro.yaml +++ b/.github/workflows/build-docker-pro.yaml @@ -44,7 +44,7 @@ jobs: repository: dbgate/dbgate-pro token: '${{ secrets.GH_TOKEN }}' path: dbgate-pro - ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 + ref: a2f824dc711b510a5e8235d3faf4aafab1965184 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/build-npm-pro.yaml b/.github/workflows/build-npm-pro.yaml index cb378c1d1..fc876f443 100644 --- a/.github/workflows/build-npm-pro.yaml +++ b/.github/workflows/build-npm-pro.yaml @@ -32,7 +32,7 @@ jobs: repository: dbgate/dbgate-pro token: '${{ secrets.GH_TOKEN }}' path: dbgate-pro - ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 + ref: a2f824dc711b510a5e8235d3faf4aafab1965184 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/.github/workflows/e2e-pro.yaml b/.github/workflows/e2e-pro.yaml index 0edb18b8b..9e3378751 100644 --- a/.github/workflows/e2e-pro.yaml +++ b/.github/workflows/e2e-pro.yaml @@ -26,7 +26,7 @@ jobs: repository: dbgate/dbgate-pro token: '${{ secrets.GH_TOKEN }}' path: dbgate-pro - ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 + ref: a2f824dc711b510a5e8235d3faf4aafab1965184 - name: Merge dbgate/dbgate-pro run: | mkdir ../dbgate-pro diff --git a/e2e-tests/cypress/e2e/browse-data.cy.js b/e2e-tests/cypress/e2e/browse-data.cy.js index 51a0127cb..b1dd797ef 100644 --- a/e2e-tests/cypress/e2e/browse-data.cy.js +++ b/e2e-tests/cypress/e2e/browse-data.cy.js @@ -349,4 +349,17 @@ describe('Data browser data', () => { cy.testid('CompareModelTab_tabOperations').click(); cy.themeshot('comparesettings'); }); + + it.only('Query editor - AI assistant', () => { + cy.contains('MySql-connection').click(); + cy.contains('MyChinook').click(); + cy.testid('TabsPanel_buttonNewQuery').click(); + cy.testid('QueryTab_switchAiAssistantButton').click(); + cy.testid('QueryAiAssistant_promptInput').type('album names'); + cy.testid('QueryAiAssistant_queryFromQuestionButton').click(); + cy.contains('Use this').click(); + cy.testid('QueryTab_executeButton').click(); + cy.contains('Balls to the Wall'); + cy.themeshot('aiassistant'); + }); }); diff --git a/packages/api/src/controllers/databaseConnections.js b/packages/api/src/controllers/databaseConnections.js index fd51b5b32..57b794c11 100644 --- a/packages/api/src/controllers/databaseConnections.js +++ b/packages/api/src/controllers/databaseConnections.js @@ -34,6 +34,8 @@ const pipeForkLogs = require('../utility/pipeForkLogs'); const crypto = require('crypto'); const loadModelTransform = require('../utility/loadModelTransform'); const exportDbModelSql = require('../utility/exportDbModelSql'); +const axios = require('axios'); +const { callTextToSqlApi, callCompleteOnCursorApi, callRefactorSqlQueryApi } = require('../utility/authProxy'); const logger = getLogger('databaseConnections'); @@ -562,4 +564,47 @@ module.exports = { return true; }, + + textToSql_meta: true, + async textToSql({ conid, database, text, dialect }) { + const existing = this.opened.find(x => x.conid == conid && x.database == database); + const { structure } = existing || {}; + if (!structure) return { errorMessage: 'No database structure' }; + + const res = await callTextToSqlApi(text, structure, dialect); + + if (!res?.sql) { + return { errorMessage: 'No SQL generated' }; + } + + return res; + }, + + completeOnCursor_meta: true, + async completeOnCursor({ conid, database, text, dialect, line }) { + const existing = this.opened.find(x => x.conid == conid && x.database == database); + const { structure } = existing || {}; + if (!structure) return { errorMessage: 'No database structure' }; + const res = await callCompleteOnCursorApi(text, structure, dialect, line); + + if (!res?.variants) { + return { errorMessage: 'No SQL generated' }; + } + + return res; + }, + + refactorSqlQuery_meta: true, + async refactorSqlQuery({ conid, database, query, task, dialect }) { + const existing = this.opened.find(x => x.conid == conid && x.database == database); + const { structure } = existing || {}; + if (!structure) return { errorMessage: 'No database structure' }; + const res = await callRefactorSqlQueryApi(query, task, structure, dialect); + + if (!res?.sql) { + return { errorMessage: 'No SQL generated' }; + } + + return res; + }, }; diff --git a/packages/api/src/utility/authProxy.js b/packages/api/src/utility/authProxy.js index f8bec4025..0d998fec2 100644 --- a/packages/api/src/utility/authProxy.js +++ b/packages/api/src/utility/authProxy.js @@ -24,6 +24,18 @@ async function getAwsIamToken(params) { return null; } +async function callTextToSqlApi(text, structure, dialect) { + return null; +} + +async function callCompleteOnCursorApi(cursorId, query, position, dialect) { + return null; +} + +async function callRefactorSqlQueryApi(query, task, structure, dialect) { + return null; +} + module.exports = { isAuthProxySupported, authProxyGetRedirectUrl, @@ -32,4 +44,7 @@ module.exports = { getAuthProxyUrl, supportsAwsIam, getAwsIamToken, + callTextToSqlApi, + callCompleteOnCursorApi, + callRefactorSqlQueryApi, }; diff --git a/packages/types/dbinfo.d.ts b/packages/types/dbinfo.d.ts index 75012dbab..0f5a0b99f 100644 --- a/packages/types/dbinfo.d.ts +++ b/packages/types/dbinfo.d.ts @@ -189,3 +189,34 @@ export interface DatabaseInfoObjects { export interface DatabaseInfo extends DatabaseInfoObjects { engine?: string; } + +export interface ColumnReferenceTiny { + n: string; // name + r?: string; // ref name +} + +export interface PrimaryKeyInfoTiny { + c: ColumnReferenceTiny[]; // columns +} + +export interface ForeignKeyInfoTiny { + c: ColumnReferenceTiny[]; // columns + r: string; // reference table name +} + +export interface ColumnInfoTiny { + n: string; // name + t: string; // type +} + +export interface TableInfoTiny { + n: string; //name + o: string; // comment + c: ColumnInfoTiny[]; // columns + p?: PrimaryKeyInfoTiny; // primary key + f?: ForeignKeyInfoTiny[]; // foreign keys +} + +export interface DatabaseInfoTiny { + t: TableInfoTiny[]; // tables +} diff --git a/packages/web/public/bulma.css b/packages/web/public/bulma.css index 6492cd0c3..471553f74 100644 --- a/packages/web/public/bulma.css +++ b/packages/web/public/bulma.css @@ -2,6 +2,30 @@ margin: 0 !important; } +.m-1 { + margin: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.m-3 { + margin: 0.75rem !important; +} + +.m-4 { + margin: 1rem !important; +} + +.m-5 { + margin: 1.5rem !important; +} + +.m-6 { + margin: 3rem !important; +} + .mt-0 { margin-top: 0 !important; } @@ -28,10 +52,6 @@ margin-bottom: 0 !important; } -.m-1 { - margin: 0.25rem !important; -} - .mt-1 { margin-top: 0.25rem !important; } @@ -58,10 +78,6 @@ margin-bottom: 0.25rem !important; } -.m-2 { - margin: 0.5rem !important; -} - .mt-2 { margin-top: 0.5rem !important; } @@ -88,10 +104,6 @@ margin-bottom: 0.5rem !important; } -.m-3 { - margin: 0.75rem !important; -} - .mt-3 { margin-top: 0.75rem !important; } @@ -118,10 +130,6 @@ margin-bottom: 0.75rem !important; } -.m-4 { - margin: 1rem !important; -} - .mt-4 { margin-top: 1rem !important; } @@ -148,10 +156,6 @@ margin-bottom: 1rem !important; } -.m-5 { - margin: 1.5rem !important; -} - .mt-5 { margin-top: 1.5rem !important; } @@ -178,10 +182,6 @@ margin-bottom: 1.5rem !important; } -.m-6 { - margin: 3rem !important; -} - .mt-6 { margin-top: 3rem !important; } diff --git a/packages/web/src/buttons/ToolStripCommandSplitButton.svelte b/packages/web/src/buttons/ToolStripCommandSplitButton.svelte index 933b5fe51..0373bb8cc 100644 --- a/packages/web/src/buttons/ToolStripCommandSplitButton.svelte +++ b/packages/web/src/buttons/ToolStripCommandSplitButton.svelte @@ -17,4 +17,5 @@ {menu} {hideDisabled} {buttonLabel} + {...$$restProps} /> diff --git a/packages/web/src/buttons/ToolStripSplitButton.svelte b/packages/web/src/buttons/ToolStripSplitButton.svelte index 007ca44cd..a21253c85 100644 --- a/packages/web/src/buttons/ToolStripSplitButton.svelte +++ b/packages/web/src/buttons/ToolStripSplitButton.svelte @@ -21,7 +21,7 @@
-
+
diff --git a/packages/web/src/buttons/ToolStripSplitDropDownButton.svelte b/packages/web/src/buttons/ToolStripSplitDropDownButton.svelte index ffec46466..5236004f7 100644 --- a/packages/web/src/buttons/ToolStripSplitDropDownButton.svelte +++ b/packages/web/src/buttons/ToolStripSplitDropDownButton.svelte @@ -19,6 +19,6 @@ } - + diff --git a/packages/web/src/celldata/JsonCellView.svelte b/packages/web/src/celldata/JsonCellView.svelte index f9589a6fe..ea9633c33 100644 --- a/packages/web/src/celldata/JsonCellView.svelte +++ b/packages/web/src/celldata/JsonCellView.svelte @@ -5,6 +5,7 @@ export let selection; export let showWholeRow = false; + export let expandAll = false; let json = null; let error = null; @@ -31,7 +32,7 @@ {:else}
- +
{/if} diff --git a/packages/web/src/celldata/JsonExpandedCellView.svelte b/packages/web/src/celldata/JsonExpandedCellView.svelte new file mode 100644 index 000000000..db38ad001 --- /dev/null +++ b/packages/web/src/celldata/JsonExpandedCellView.svelte @@ -0,0 +1,6 @@ + + + diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 95feb253f..084ef9b7b 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -148,6 +148,7 @@ 'icon parent-filter-outline': 'mdi mdi-home-alert-outline', 'icon download': 'mdi mdi-download', 'icon text': 'mdi mdi-text', + 'icon ai': 'mdi mdi-head-lightbulb', 'icon run': 'mdi mdi-play', 'icon chevron-down': 'mdi mdi-chevron-down', diff --git a/packages/web/src/modals/ConfirmModal.svelte b/packages/web/src/modals/ConfirmModal.svelte index 56cafc438..dceb19281 100644 --- a/packages/web/src/modals/ConfirmModal.svelte +++ b/packages/web/src/modals/ConfirmModal.svelte @@ -21,8 +21,14 @@ closeCurrentModal(); onConfirm(); }} + data-testid="ConfirmModal_okButton" + /> + - diff --git a/packages/web/src/query/QueryAiAssistant.svelte b/packages/web/src/query/QueryAiAssistant.svelte new file mode 100644 index 000000000..b44912f77 --- /dev/null +++ b/packages/web/src/query/QueryAiAssistant.svelte @@ -0,0 +1 @@ +AI Assistant \ No newline at end of file diff --git a/packages/web/src/settings/SettingsModal.svelte b/packages/web/src/settings/SettingsModal.svelte index 5cff81e87..e24e31805 100644 --- a/packages/web/src/settings/SettingsModal.svelte +++ b/packages/web/src/settings/SettingsModal.svelte @@ -410,6 +410,14 @@ ORDER BY { value: 'download', label: 'Check and download new versions' }, ]} /> + + {#if isProApp()} + + {/if} diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts index 2ab628c22..e49b4d394 100644 --- a/packages/web/src/stores.ts +++ b/packages/web/src/stores.ts @@ -115,6 +115,7 @@ export const currentEditorFontSize = getElectron() ? writableSettingsValue(null, 'currentEditorFontSize') : writableWithStorage(null, 'currentEditorFontSize'); export const currentEditorFont = writableSettingsValue(null, 'editor.fontFamily'); +export const allowedSendToAiService = writableSettingsValue(false, 'ai.allowSendModels'); export const activeTabId = derived([openedTabs], ([$openedTabs]) => $openedTabs.find(x => x.selected)?.tabid); export const activeTab = derived([openedTabs], ([$openedTabs]) => $openedTabs.find(x => x.selected)); export const recentDatabases = writableWithStorage([], 'recentDatabases'); diff --git a/packages/web/src/tabs/QueryTab.svelte b/packages/web/src/tabs/QueryTab.svelte index b8198c89b..9113ccd5c 100644 --- a/packages/web/src/tabs/QueryTab.svelte +++ b/packages/web/src/tabs/QueryTab.svelte @@ -12,6 +12,15 @@ testEnabled: () => getCurrentEditor()?.isSqlEditor(), onClick: () => getCurrentEditor().formatCode(), }); + registerCommand({ + id: 'query.switchAiAssistant', + category: 'Query', + name: 'AI Assistant', + keyText: 'Shift+Alt+A', + icon: 'icon ai', + testEnabled: () => isProApp(), + onClick: () => getCurrentEditor().toggleAiAssistant(), + }); registerCommand({ id: 'query.insertSqlJoin', category: 'Query', @@ -89,6 +98,9 @@ import ToolStripDropDownButton from '../buttons/ToolStripDropDownButton.svelte'; import { extractQueryParameters, replaceQueryParameters } from 'dbgate-query-splitter'; import QueryParametersModal from '../modals/QueryParametersModal.svelte'; + import { isProApp } from '../utility/proTools'; + import HorizontalSplitter from '../elements/HorizontalSplitter.svelte'; + import QueryAiAssistant from '../query/QueryAiAssistant.svelte'; export let tabid; export let conid; @@ -137,6 +149,7 @@ let domEditor; let domToolStrip; let intervalId; + let isAiAssistantVisible = localStorage.getItem(`tabdata_isAiAssistantVisible_${tabid}`) == 'true'; onMount(() => { intervalId = setInterval(() => { @@ -210,6 +223,10 @@ visibleResultTabs = !visibleResultTabs; } + export function toggleAiAssistant() { + isAiAssistantVisible = !isAiAssistantVisible; + } + function getParameterSplitterOptions() { if (!queryParameterStyle) { return null; @@ -401,6 +418,7 @@ { command: 'query.replace' }, { divider: true }, { command: 'query.toggleVisibleResultTabs' }, + { command: 'query.switchAiAssistant', hideDisabled: true }, ]; } @@ -420,79 +438,116 @@ localStorage.getItem(`tabdata_queryParamStyle_${tabid}`) ?? initialArgs?.queryParameterStyle ?? (initialArgs?.scriptTemplate == 'CALL OBJECT' ? ':' : ''); + + $: localStorage.setItem(`tabdata_isAiAssistantVisible_${tabid}`, isAiAssistantVisible ? 'true' : 'false'); - + - {#if driver?.databaseEngineTypes?.includes('sql')} - { - setEditorData(e.detail); - if (isInitialized) { - markTabUnsaved(tabid); - } - errorMessages = []; - }} - on:focus={() => { - activator.activate(); - domToolStrip?.activate(); - invalidateCommands(); - setTimeout(() => { - isInitialized = true; - }, 100); - }} - bind:this={domEditor} - onExecuteFragment={(sql, startLine) => executeCore(sql, startLine)} - {errorMessages} - /> - {:else} - setEditorData(e.detail)} - on:focus={() => { - activator.activate(); - domToolStrip?.activate(); - invalidateCommands(); - }} - bind:this={domEditor} - /> - {/if} + + + {#if driver?.databaseEngineTypes?.includes('sql')} + { + setEditorData(e.detail); + if (isInitialized) { + markTabUnsaved(tabid); + } + errorMessages = []; + }} + on:focus={() => { + activator.activate(); + domToolStrip?.activate(); + invalidateCommands(); + setTimeout(() => { + isInitialized = true; + }, 100); + }} + bind:this={domEditor} + onExecuteFragment={(sql, startLine) => executeCore(sql, startLine)} + {errorMessages} + /> + {:else} + setEditorData(e.detail)} + on:focus={() => { + activator.activate(); + domToolStrip?.activate(); + invalidateCommands(); + }} + bind:this={domEditor} + /> + {/if} + + + + + + + + + - - - - - + { + isAiAssistantVisible = false; + }} + text={$editorValue} + getLine={() => domEditor.getEditor().getSelectionRange().start.row} + onInsertAtCursor={text => { + const editor = domEditor.getEditor(); + editor.session.insert(editor.getCursorPosition(), text); + domEditor?.getEditor()?.focus(); + }} + getTextOrSelectedText={() => domEditor.getEditor().getSelectedText() || $editorValue} + onSetSelectedText={text => { + const editor = domEditor.getEditor(); + if (editor.getSelectedText()) { + const range = editor.selection.getRange(); + editor.session.replace(range, text); + } else { + editor.setValue(text); + } + }} + {tabid} + /> - + - - + + {#if resultCount == 1} @@ -511,6 +566,13 @@ icon="icon at" title="Query parameter style" /> + + AI Assistant + diff --git a/packages/web/src/widgets/CellDataWidget.svelte b/packages/web/src/widgets/CellDataWidget.svelte index 367948296..379fd3f85 100644 --- a/packages/web/src/widgets/CellDataWidget.svelte +++ b/packages/web/src/widgets/CellDataWidget.svelte @@ -20,6 +20,12 @@ component: JsonCellView, single: true, }, + { + type: 'jsonExpanded', + title: 'Json - expanded', + component: JsonExpandedCellView, + single: true, + }, { type: 'jsonRow', title: 'Json - Row', @@ -84,6 +90,7 @@ import SelectField from '../forms/SelectField.svelte'; import { selectedCellsCallback } from '../stores'; import WidgetTitle from './WidgetTitle.svelte'; + import JsonExpandedCellView from '../celldata/JsonExpandedCellView.svelte'; let selectedFormatType = 'autodetect'; @@ -107,6 +114,7 @@ isNative value={selectedFormatType} on:change={e => (selectedFormatType = e.detail)} + data-testid="CellDataWidget_selectFormat" options={[ { value: 'autodetect', label: `Autodetect - ${autodetectFormat.title}` }, ...formats.map(fmt => ({ label: fmt.title, value: fmt.type })), diff --git a/packages/web/src/widgets/WidgetColumnBarItem.svelte b/packages/web/src/widgets/WidgetColumnBarItem.svelte index f973957cd..f52f95654 100644 --- a/packages/web/src/widgets/WidgetColumnBarItem.svelte +++ b/packages/web/src/widgets/WidgetColumnBarItem.svelte @@ -16,6 +16,7 @@ export let collapsed = null; export let storageName = null; + export let onClose = null; let size = 0; @@ -70,7 +71,8 @@ (visible = !visible) : null} - data-testid={$$props['data-testid']}>{title}{title} {#if visible} diff --git a/packages/web/src/widgets/WidgetTitle.svelte b/packages/web/src/widgets/WidgetTitle.svelte index a4a3bd0f3..b7c692277 100644 --- a/packages/web/src/widgets/WidgetTitle.svelte +++ b/packages/web/src/widgets/WidgetTitle.svelte @@ -1,18 +1,35 @@ -
+
+ {#if onClose} +
+ +
+ {/if}