Merge branch 'master' into feature/cassandra

This commit is contained in:
SPRINX0\prochazka
2025-02-10 14:23:16 +01:00
26 changed files with 321 additions and 103 deletions

View File

@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: '${{ secrets.GH_TOKEN }}' token: '${{ secrets.GH_TOKEN }}'
path: dbgate-pro path: dbgate-pro
ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 ref: a2f824dc711b510a5e8235d3faf4aafab1965184
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro

View File

@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: '${{ secrets.GH_TOKEN }}' token: '${{ secrets.GH_TOKEN }}'
path: dbgate-pro path: dbgate-pro
ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 ref: a2f824dc711b510a5e8235d3faf4aafab1965184
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro

View File

@@ -36,7 +36,7 @@ jobs:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: '${{ secrets.GH_TOKEN }}' token: '${{ secrets.GH_TOKEN }}'
path: dbgate-pro path: dbgate-pro
ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 ref: a2f824dc711b510a5e8235d3faf4aafab1965184
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro

View File

@@ -44,7 +44,7 @@ jobs:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: '${{ secrets.GH_TOKEN }}' token: '${{ secrets.GH_TOKEN }}'
path: dbgate-pro path: dbgate-pro
ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 ref: a2f824dc711b510a5e8235d3faf4aafab1965184
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro

View File

@@ -32,7 +32,7 @@ jobs:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: '${{ secrets.GH_TOKEN }}' token: '${{ secrets.GH_TOKEN }}'
path: dbgate-pro path: dbgate-pro
ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 ref: a2f824dc711b510a5e8235d3faf4aafab1965184
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro

View File

@@ -26,7 +26,7 @@ jobs:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: '${{ secrets.GH_TOKEN }}' token: '${{ secrets.GH_TOKEN }}'
path: dbgate-pro path: dbgate-pro
ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 ref: a2f824dc711b510a5e8235d3faf4aafab1965184
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro

View File

@@ -349,4 +349,17 @@ describe('Data browser data', () => {
cy.testid('CompareModelTab_tabOperations').click(); cy.testid('CompareModelTab_tabOperations').click();
cy.themeshot('comparesettings'); 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');
});
}); });

View File

@@ -34,6 +34,8 @@ const pipeForkLogs = require('../utility/pipeForkLogs');
const crypto = require('crypto'); const crypto = require('crypto');
const loadModelTransform = require('../utility/loadModelTransform'); const loadModelTransform = require('../utility/loadModelTransform');
const exportDbModelSql = require('../utility/exportDbModelSql'); const exportDbModelSql = require('../utility/exportDbModelSql');
const axios = require('axios');
const { callTextToSqlApi, callCompleteOnCursorApi, callRefactorSqlQueryApi } = require('../utility/authProxy');
const logger = getLogger('databaseConnections'); const logger = getLogger('databaseConnections');
@@ -562,4 +564,47 @@ module.exports = {
return true; 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;
},
}; };

View File

@@ -24,6 +24,18 @@ async function getAwsIamToken(params) {
return null; 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 = { module.exports = {
isAuthProxySupported, isAuthProxySupported,
authProxyGetRedirectUrl, authProxyGetRedirectUrl,
@@ -32,4 +44,7 @@ module.exports = {
getAuthProxyUrl, getAuthProxyUrl,
supportsAwsIam, supportsAwsIam,
getAwsIamToken, getAwsIamToken,
callTextToSqlApi,
callCompleteOnCursorApi,
callRefactorSqlQueryApi,
}; };

View File

@@ -189,3 +189,34 @@ export interface DatabaseInfoObjects {
export interface DatabaseInfo extends DatabaseInfoObjects { export interface DatabaseInfo extends DatabaseInfoObjects {
engine?: string; 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
}

View File

@@ -2,6 +2,30 @@
margin: 0 !important; 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 { .mt-0 {
margin-top: 0 !important; margin-top: 0 !important;
} }
@@ -28,10 +52,6 @@
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.m-1 {
margin: 0.25rem !important;
}
.mt-1 { .mt-1 {
margin-top: 0.25rem !important; margin-top: 0.25rem !important;
} }
@@ -58,10 +78,6 @@
margin-bottom: 0.25rem !important; margin-bottom: 0.25rem !important;
} }
.m-2 {
margin: 0.5rem !important;
}
.mt-2 { .mt-2 {
margin-top: 0.5rem !important; margin-top: 0.5rem !important;
} }
@@ -88,10 +104,6 @@
margin-bottom: 0.5rem !important; margin-bottom: 0.5rem !important;
} }
.m-3 {
margin: 0.75rem !important;
}
.mt-3 { .mt-3 {
margin-top: 0.75rem !important; margin-top: 0.75rem !important;
} }
@@ -118,10 +130,6 @@
margin-bottom: 0.75rem !important; margin-bottom: 0.75rem !important;
} }
.m-4 {
margin: 1rem !important;
}
.mt-4 { .mt-4 {
margin-top: 1rem !important; margin-top: 1rem !important;
} }
@@ -148,10 +156,6 @@
margin-bottom: 1rem !important; margin-bottom: 1rem !important;
} }
.m-5 {
margin: 1.5rem !important;
}
.mt-5 { .mt-5 {
margin-top: 1.5rem !important; margin-top: 1.5rem !important;
} }
@@ -178,10 +182,6 @@
margin-bottom: 1.5rem !important; margin-bottom: 1.5rem !important;
} }
.m-6 {
margin: 3rem !important;
}
.mt-6 { .mt-6 {
margin-top: 3rem !important; margin-top: 3rem !important;
} }

View File

@@ -17,4 +17,5 @@
{menu} {menu}
{hideDisabled} {hideDisabled}
{buttonLabel} {buttonLabel}
{...$$restProps}
/> />

View File

@@ -21,7 +21,7 @@
<div class="button" class:disabled {title}> <div class="button" class:disabled {title}>
<div class="inner" class:disabled> <div class="inner" class:disabled>
<div class="main" class:disabled on:click={handleClick}> <div class="main" class:disabled on:click={handleClick} data-testid={$$props['data-testid']}>
<span class="icon" class:disabled><FontIcon {icon} /></span> <span class="icon" class:disabled><FontIcon {icon} /></span>
<slot /> <slot />
</div> </div>

View File

@@ -19,6 +19,6 @@
} }
</script> </script>
<ToolStripSplitButton {title} {icon} {disabled} on:splitclick={handleClick} on:click> <ToolStripSplitButton {title} {icon} {disabled} on:splitclick={handleClick} on:click {...$$restProps}>
<slot /> <slot />
</ToolStripSplitButton> </ToolStripSplitButton>

View File

@@ -5,6 +5,7 @@
export let selection; export let selection;
export let showWholeRow = false; export let showWholeRow = false;
export let expandAll = false;
let json = null; let json = null;
let error = null; let error = null;
@@ -31,7 +32,7 @@
{:else} {:else}
<div class="outer"> <div class="outer">
<div class="inner"> <div class="inner">
<JSONTree value={json} expanded /> <JSONTree value={json} {expandAll} expanded />
</div> </div>
</div> </div>
{/if} {/if}

View File

@@ -0,0 +1,6 @@
<script lang="ts">
import JsonCellView from './JsonCellView.svelte';
export let selection;
</script>
<JsonCellView {selection} expandAll />

View File

@@ -148,6 +148,7 @@
'icon parent-filter-outline': 'mdi mdi-home-alert-outline', 'icon parent-filter-outline': 'mdi mdi-home-alert-outline',
'icon download': 'mdi mdi-download', 'icon download': 'mdi mdi-download',
'icon text': 'mdi mdi-text', 'icon text': 'mdi mdi-text',
'icon ai': 'mdi mdi-head-lightbulb',
'icon run': 'mdi mdi-play', 'icon run': 'mdi mdi-play',
'icon chevron-down': 'mdi mdi-chevron-down', 'icon chevron-down': 'mdi mdi-chevron-down',

View File

@@ -21,8 +21,14 @@
closeCurrentModal(); closeCurrentModal();
onConfirm(); onConfirm();
}} }}
data-testid="ConfirmModal_okButton"
/>
<FormStyledButton
type="button"
value="Close"
on:click={closeCurrentModal}
data-testid="ConfirmModal_closeButton"
/> />
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />
</svelte:fragment> </svelte:fragment>
</ModalBase> </ModalBase>
</FormProvider> </FormProvider>

View File

@@ -0,0 +1 @@
AI Assistant

View File

@@ -410,6 +410,14 @@ ORDER BY
{ value: 'download', label: 'Check and download new versions' }, { value: 'download', label: 'Check and download new versions' },
]} ]}
/> />
{#if isProApp()}
<FormCheckboxField
name="ai.allowSendModels"
label="Allow to send DB models and query snippets to AI service"
defaultValue={false}
/>
{/if}
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="7"> <svelte:fragment slot="7">

View File

@@ -115,6 +115,7 @@ export const currentEditorFontSize = getElectron()
? writableSettingsValue(null, 'currentEditorFontSize') ? writableSettingsValue(null, 'currentEditorFontSize')
: writableWithStorage(null, 'currentEditorFontSize'); : writableWithStorage(null, 'currentEditorFontSize');
export const currentEditorFont = writableSettingsValue(null, 'editor.fontFamily'); 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 activeTabId = derived([openedTabs], ([$openedTabs]) => $openedTabs.find(x => x.selected)?.tabid);
export const activeTab = derived([openedTabs], ([$openedTabs]) => $openedTabs.find(x => x.selected)); export const activeTab = derived([openedTabs], ([$openedTabs]) => $openedTabs.find(x => x.selected));
export const recentDatabases = writableWithStorage([], 'recentDatabases'); export const recentDatabases = writableWithStorage([], 'recentDatabases');

View File

@@ -12,6 +12,15 @@
testEnabled: () => getCurrentEditor()?.isSqlEditor(), testEnabled: () => getCurrentEditor()?.isSqlEditor(),
onClick: () => getCurrentEditor().formatCode(), 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({ registerCommand({
id: 'query.insertSqlJoin', id: 'query.insertSqlJoin',
category: 'Query', category: 'Query',
@@ -89,6 +98,9 @@
import ToolStripDropDownButton from '../buttons/ToolStripDropDownButton.svelte'; import ToolStripDropDownButton from '../buttons/ToolStripDropDownButton.svelte';
import { extractQueryParameters, replaceQueryParameters } from 'dbgate-query-splitter'; import { extractQueryParameters, replaceQueryParameters } from 'dbgate-query-splitter';
import QueryParametersModal from '../modals/QueryParametersModal.svelte'; 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 tabid;
export let conid; export let conid;
@@ -137,6 +149,7 @@
let domEditor; let domEditor;
let domToolStrip; let domToolStrip;
let intervalId; let intervalId;
let isAiAssistantVisible = localStorage.getItem(`tabdata_isAiAssistantVisible_${tabid}`) == 'true';
onMount(() => { onMount(() => {
intervalId = setInterval(() => { intervalId = setInterval(() => {
@@ -210,6 +223,10 @@
visibleResultTabs = !visibleResultTabs; visibleResultTabs = !visibleResultTabs;
} }
export function toggleAiAssistant() {
isAiAssistantVisible = !isAiAssistantVisible;
}
function getParameterSplitterOptions() { function getParameterSplitterOptions() {
if (!queryParameterStyle) { if (!queryParameterStyle) {
return null; return null;
@@ -401,6 +418,7 @@
{ command: 'query.replace' }, { command: 'query.replace' },
{ divider: true }, { divider: true },
{ command: 'query.toggleVisibleResultTabs' }, { command: 'query.toggleVisibleResultTabs' },
{ command: 'query.switchAiAssistant', hideDisabled: true },
]; ];
} }
@@ -420,79 +438,116 @@
localStorage.getItem(`tabdata_queryParamStyle_${tabid}`) ?? localStorage.getItem(`tabdata_queryParamStyle_${tabid}`) ??
initialArgs?.queryParameterStyle ?? initialArgs?.queryParameterStyle ??
(initialArgs?.scriptTemplate == 'CALL OBJECT' ? ':' : ''); (initialArgs?.scriptTemplate == 'CALL OBJECT' ? ':' : '');
$: localStorage.setItem(`tabdata_isAiAssistantVisible_${tabid}`, isAiAssistantVisible ? 'true' : 'false');
</script> </script>
<ToolStripContainer bind:this={domToolStrip}> <ToolStripContainer bind:this={domToolStrip}>
<VerticalSplitter isSplitter={visibleResultTabs}> <HorizontalSplitter isSplitter={isAiAssistantVisible} initialSizeRight={300}>
<svelte:fragment slot="1"> <svelte:fragment slot="1">
{#if driver?.databaseEngineTypes?.includes('sql')} <VerticalSplitter isSplitter={visibleResultTabs}>
<SqlEditor <svelte:fragment slot="1">
engine={$connection && $connection.engine} {#if driver?.databaseEngineTypes?.includes('sql')}
{conid} <SqlEditor
{database} engine={$connection && $connection.engine}
splitterOptions={driver?.getQuerySplitterOptions('editor')} {conid}
options={{ {database}
wrap: enableWrap, splitterOptions={driver?.getQuerySplitterOptions('editor')}
}} options={{
value={$editorState.value || ''} wrap: enableWrap,
menu={createMenu()} }}
on:input={e => { value={$editorState.value || ''}
setEditorData(e.detail); menu={createMenu()}
if (isInitialized) { on:input={e => {
markTabUnsaved(tabid); setEditorData(e.detail);
} if (isInitialized) {
errorMessages = []; markTabUnsaved(tabid);
}} }
on:focus={() => { errorMessages = [];
activator.activate(); }}
domToolStrip?.activate(); on:focus={() => {
invalidateCommands(); activator.activate();
setTimeout(() => { domToolStrip?.activate();
isInitialized = true; invalidateCommands();
}, 100); setTimeout(() => {
}} isInitialized = true;
bind:this={domEditor} }, 100);
onExecuteFragment={(sql, startLine) => executeCore(sql, startLine)} }}
{errorMessages} bind:this={domEditor}
/> onExecuteFragment={(sql, startLine) => executeCore(sql, startLine)}
{:else} {errorMessages}
<AceEditor />
mode={driver?.editorMode || 'sql'} {:else}
value={$editorState.value || ''} <AceEditor
splitterOptions={driver?.getQuerySplitterOptions('editor')} mode={driver?.editorMode || 'sql'}
options={{ value={$editorState.value || ''}
wrap: enableWrap, splitterOptions={driver?.getQuerySplitterOptions('editor')}
}} options={{
menu={createMenu()} wrap: enableWrap,
on:input={e => setEditorData(e.detail)} }}
on:focus={() => { menu={createMenu()}
activator.activate(); on:input={e => setEditorData(e.detail)}
domToolStrip?.activate(); on:focus={() => {
invalidateCommands(); activator.activate();
}} domToolStrip?.activate();
bind:this={domEditor} invalidateCommands();
/> }}
{/if} bind:this={domEditor}
/>
{/if}
</svelte:fragment>
<svelte:fragment slot="2">
<ResultTabs tabs={[{ label: 'Messages', slot: 0 }]} {sessionId} {executeNumber} bind:resultCount {driver}>
<svelte:fragment slot="0">
<SocketMessageView
eventName={sessionId ? `session-info-${sessionId}` : null}
onMessageClick={handleMesageClick}
{executeNumber}
startLine={executeStartLine}
showProcedure
showLine
onChangeErrors={handleChangeErrors}
/>
</svelte:fragment>
</ResultTabs>
</svelte:fragment>
</VerticalSplitter>
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="2"> <svelte:fragment slot="2">
<ResultTabs tabs={[{ label: 'Messages', slot: 0 }]} {sessionId} {executeNumber} bind:resultCount {driver}> <QueryAiAssistant
<svelte:fragment slot="0"> {conid}
<SocketMessageView {database}
eventName={sessionId ? `session-info-${sessionId}` : null} {driver}
onMessageClick={handleMesageClick} onClose={() => {
{executeNumber} isAiAssistantVisible = false;
startLine={executeStartLine} }}
showProcedure text={$editorValue}
showLine getLine={() => domEditor.getEditor().getSelectionRange().start.row}
onChangeErrors={handleChangeErrors} onInsertAtCursor={text => {
/> const editor = domEditor.getEditor();
</svelte:fragment> editor.session.insert(editor.getCursorPosition(), text);
</ResultTabs> 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}
/>
</svelte:fragment> </svelte:fragment>
</VerticalSplitter> </HorizontalSplitter>
<svelte:fragment slot="toolstrip"> <svelte:fragment slot="toolstrip">
<ToolStripCommandSplitButton commands={['query.execute', 'query.executeCurrent']} /> <ToolStripCommandSplitButton
<ToolStripCommandButton command="query.kill" /> commands={['query.execute', 'query.executeCurrent']}
data-testid="QueryTab_executeButton"
/>
<ToolStripCommandButton command="query.kill" data-testid="QueryTab_killButton" />
<ToolStripSaveButton idPrefix="query" /> <ToolStripSaveButton idPrefix="query" />
<ToolStripCommandButton command="query.formatCode" /> <ToolStripCommandButton command="query.formatCode" />
{#if resultCount == 1} {#if resultCount == 1}
@@ -511,6 +566,13 @@
icon="icon at" icon="icon at"
title="Query parameter style" title="Query parameter style"
/> />
<ToolStripCommandButton
command="query.switchAiAssistant"
hideDisabled
data-testid="QueryTab_switchAiAssistantButton"
>
AI Assistant
</ToolStripCommandButton>
</svelte:fragment> </svelte:fragment>
</ToolStripContainer> </ToolStripContainer>

View File

@@ -20,6 +20,12 @@
component: JsonCellView, component: JsonCellView,
single: true, single: true,
}, },
{
type: 'jsonExpanded',
title: 'Json - expanded',
component: JsonExpandedCellView,
single: true,
},
{ {
type: 'jsonRow', type: 'jsonRow',
title: 'Json - Row', title: 'Json - Row',
@@ -84,6 +90,7 @@
import SelectField from '../forms/SelectField.svelte'; import SelectField from '../forms/SelectField.svelte';
import { selectedCellsCallback } from '../stores'; import { selectedCellsCallback } from '../stores';
import WidgetTitle from './WidgetTitle.svelte'; import WidgetTitle from './WidgetTitle.svelte';
import JsonExpandedCellView from '../celldata/JsonExpandedCellView.svelte';
let selectedFormatType = 'autodetect'; let selectedFormatType = 'autodetect';
@@ -107,6 +114,7 @@
isNative isNative
value={selectedFormatType} value={selectedFormatType}
on:change={e => (selectedFormatType = e.detail)} on:change={e => (selectedFormatType = e.detail)}
data-testid="CellDataWidget_selectFormat"
options={[ options={[
{ value: 'autodetect', label: `Autodetect - ${autodetectFormat.title}` }, { value: 'autodetect', label: `Autodetect - ${autodetectFormat.title}` },
...formats.map(fmt => ({ label: fmt.title, value: fmt.type })), ...formats.map(fmt => ({ label: fmt.title, value: fmt.type })),

View File

@@ -16,6 +16,7 @@
export let collapsed = null; export let collapsed = null;
export let storageName = null; export let storageName = null;
export let onClose = null;
let size = 0; let size = 0;
@@ -70,7 +71,8 @@
<WidgetTitle <WidgetTitle
clickable={collapsible} clickable={collapsible}
on:click={collapsible ? () => (visible = !visible) : null} on:click={collapsible ? () => (visible = !visible) : null}
data-testid={$$props['data-testid']}>{title}</WidgetTitle data-testid={$$props['data-testid']}
{onClose}>{title}</WidgetTitle
> >
{#if visible} {#if visible}

View File

@@ -1,18 +1,35 @@
<script lang="ts"> <script lang="ts">
import FontIcon from '../icons/FontIcon.svelte';
export let clickable = false; export let clickable = false;
export let onClose = null;
</script> </script>
<div on:click class:clickable {...$$restProps}> <div on:click class:clickable {...$$restProps} class="wrapper">
<slot /> <slot />
{#if onClose}
<div class="close" on:click={onClose}>
<FontIcon icon="icon close" />
</div>
{/if}
</div> </div>
<style> <style>
div { .wrapper {
padding: 5px; padding: 5px;
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
background-color: var(--theme-bg-1); background-color: var(--theme-bg-1);
border: 2px solid var(--theme-border); border: 2px solid var(--theme-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.close {
cursor: pointer;
}
.close:hover {
color: var(--theme-font-hover);
} }
div.clickable:hover { div.clickable:hover {
background-color: var(--theme-bg-2); background-color: var(--theme-bg-2);

View File

@@ -7,7 +7,7 @@ checkout-and-merge-pro:
repository: dbgate/dbgate-pro repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }} token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro path: dbgate-pro
ref: a770b7e7a4d0ced5f1ade7cba4ba516220765648 ref: a2f824dc711b510a5e8235d3faf4aafab1965184
- name: Merge dbgate/dbgate-pro - name: Merge dbgate/dbgate-pro
run: | run: |
mkdir ../dbgate-pro mkdir ../dbgate-pro