Merge branch 'develop'

This commit is contained in:
Jan Prochazka
2022-01-29 19:31:12 +01:00
53 changed files with 1543 additions and 130 deletions

View File

@@ -15,6 +15,7 @@
import { subscribeConnectionPingers } from './utility/connectionsPinger';
import { subscribePermissionCompiler } from './utility/hasPermission';
import { apiCall } from './utility/api';
import { getUsedApps } from './utility/metadataLoaders';
let loadedApi = false;
@@ -30,7 +31,8 @@
const settings = await apiCall('config/get-settings');
const connections = await apiCall('connections/list');
const config = await apiCall('config/get');
loadedApi = settings && connections && config;
const apps = await getUsedApps();
loadedApi = settings && connections && config && apps;
if (loadedApi) {
subscribeApiDependendStores();

View File

@@ -26,6 +26,12 @@
$: currentThemeType = $currentThemeDefinition?.themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light';
</script>
<svelte:head>
{#if $currentThemeDefinition?.themeCss}
{@html `<style id="themePlugin">${$currentThemeDefinition?.themeCss}</style>`}
{/if}
</svelte:head>
<div
class={`${$currentTheme} ${currentThemeType} root dbgate-screen`}
use:dragDropFileTarget

View File

@@ -0,0 +1,119 @@
<script lang="ts" context="module">
async function openTextFile(fileName, fileType, folderName, tabComponent, icon) {
const connProps: any = {};
let tooltip = undefined;
const resp = await apiCall('files/load', {
folder: 'app:' + folderName,
file: fileName + '.' + fileType,
format: 'text',
});
openNewTab(
{
title: fileName,
icon,
tabComponent,
tooltip,
props: {
savedFile:fileName + '.' + fileType,
savedFolder: 'app:' + folderName,
savedFormat: 'text',
appFolder: folderName,
...connProps,
},
},
{ editor: resp }
);
}
export const extractKey = data => data.fileName;
export const createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
const APP_ICONS = {
'config.json': 'img json',
'command.sql': 'img app-command',
'query.sql': 'img app-query',
};
function getAppIcon(data) {
return APP_ICONS[data.fileType];
}
</script>
<script lang="ts">
import _ from 'lodash';
import { filterName } from 'dbgate-tools';
import { showModal } from '../modals/modalTools';
import openNewTab from '../utility/openNewTab';
import AppObjectCore from './AppObjectCore.svelte';
import InputTextModal from '../modals/InputTextModal.svelte';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import { apiCall } from '../utility/api';
import { currentDatabase, currentDatabase } from '../stores';
export let data;
const handleRename = () => {
showModal(InputTextModal, {
value: data.fileName,
label: 'New file name',
header: 'Rename file',
onConfirm: newFile => {
apiCall('apps/rename-file', {
file: data.fileName,
folder: data.folderName,
fileType: data.fileType,
newFile,
});
},
});
};
const handleDelete = () => {
showModal(ConfirmModal, {
message: `Really delete file ${data.fileName}?`,
onConfirm: () => {
apiCall('apps/delete-file', {
file: data.fileName,
folder: data.folderName,
fileType: data.fileType,
});
},
});
};
const handleClick = () => {
if (data.fileType.endsWith('.sql')) {
handleOpenSqlFile();
}
if (data.fileType.endsWith('.json')) {
handleOpenJsonFile();
}
};
const handleOpenSqlFile = () => {
openTextFile(data.fileName, data.fileType, data.folderName, 'QueryTab', 'img sql-file');
};
const handleOpenJsonFile = () => {
openTextFile(data.fileName, data.fileType, data.folderName, 'JsonEditorTab', 'img json');
};
function createMenu() {
return [
{ text: 'Delete', onClick: handleDelete },
{ text: 'Rename', onClick: handleRename },
data.fileType.endsWith('.sql') && { text: 'Open SQL', onClick: handleOpenSqlFile },
data.fileType.endsWith('.json') && { text: 'Open JSON', onClick: handleOpenJsonFile },
// data.fileType.endsWith('.yaml') && { text: 'Open YAML', onClick: handleOpenYamlFile },
];
}
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.fileLabel}
icon={getAppIcon(data)}
menu={createMenu}
on:click={handleClick}
/>

View File

@@ -0,0 +1,96 @@
<script lang="ts" context="module">
export const extractKey = data => data.name;
export const createMatcher = data => filter => filterName(filter, data.name);
</script>
<script lang="ts">
import _, { find } from 'lodash';
import { filterName } from 'dbgate-tools';
import { currentApplication, currentDatabase } from '../stores';
import AppObjectCore from './AppObjectCore.svelte';
import { showModal } from '../modals/modalTools';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import InputTextModal from '../modals/InputTextModal.svelte';
import { apiCall } from '../utility/api';
import { useConnectionList } from '../utility/metadataLoaders';
export let data;
$: connections = useConnectionList();
const handleDelete = () => {
showModal(ConfirmModal, {
message: `Really delete application ${data.name}?`,
onConfirm: () => {
apiCall('apps/delete-folder', { folder: data.name });
},
});
};
const handleRename = () => {
const { name } = data;
showModal(InputTextModal, {
value: name,
label: 'New application name',
header: 'Rename application',
onConfirm: async newFolder => {
await apiCall('apps/rename-folder', {
folder: data.name,
newFolder: newFolder,
});
if ($currentApplication == data.name) {
$currentApplication = newFolder;
}
},
});
};
function setOnCurrentDb(value) {
apiCall('connections/update-database', {
conid: $currentDatabase?.connection?._id,
database: $currentDatabase?.name,
values: {
[`useApp:${data.name}`]: value,
},
});
}
function createMenu() {
return [
{ text: 'Delete', onClick: handleDelete },
{ text: 'Rename', onClick: handleRename },
$currentDatabase && [
!isOnCurrentDb($currentDatabase, $connections) && {
text: 'Enable on current database',
onClick: () => setOnCurrentDb(true),
},
isOnCurrentDb($currentDatabase, $connections) && {
text: 'Disable on current database',
onClick: () => setOnCurrentDb(false),
},
],
];
}
function isOnCurrentDb(currentDb, connections) {
const conn = connections.find(x => x._id == currentDb?.connection?._id);
const db = conn?.databases?.find(x => x.name == currentDb?.name);
return db && db[`useApp:${data.name}`];
}
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.name}
icon={'img app'}
statusIcon={isOnCurrentDb($currentDatabase, $connections) ? 'icon check' : null}
statusTitle={`Application ${data.name} is used for database ${$currentDatabase?.name}`}
isBold={data.name == $currentApplication}
on:click={() => ($currentApplication = data.name)}
menu={createMenu}
/>

View File

@@ -81,7 +81,7 @@
markArchiveFileAsDataSheet,
markArchiveFileAsReadonly,
} from '../utility/archiveTools';
import { apiCall } from '../utility/api';
import { apiCall } from '../utility/api';
export let data;

View File

@@ -26,7 +26,7 @@
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
import getElectron from '../utility/getElectron';
import getConnectionLabel from '../utility/getConnectionLabel';
import { getDatabaseList } from '../utility/metadataLoaders';
import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders';
import { getLocalStorage } from '../utility/storageCache';
import { apiCall } from '../utility/api';
@@ -97,7 +97,7 @@
value: 'newdb',
label: 'Database name',
onConfirm: name =>
apiCall('server-connections/create-database', {
apiCall('server-connections/create-database', {
conid: data._id,
name,
}),
@@ -153,7 +153,7 @@
],
data.singleDatabase && [
{ divider: true },
getDatabaseMenuItems(data, data.defaultDatabase, $extensions, $currentDatabase),
getDatabaseMenuItems(data, data.defaultDatabase, $extensions, $currentDatabase, $apps),
],
];
};
@@ -186,6 +186,8 @@
statusTitle = null;
}
}
$: apps = useUsedApps();
</script>
<AppObjectCore

View File

@@ -1,7 +1,8 @@
<script lang="ts" context="module">
export const extractKey = props => props.name;
export function getDatabaseMenuItems(connection, name, $extensions, $currentDatabase) {
export function getDatabaseMenuItems(connection, name, $extensions, $currentDatabase, $apps) {
const apps = filterAppsForDatabase(connection, name, $apps);
const handleNewQuery = () => {
const tooltip = `${getConnectionLabel(connection)}\n${name}`;
openNewTab({
@@ -157,8 +158,20 @@
openJsonDocument(db, name);
};
async function handleConfirmSql(sql) {
const resp = await apiCall('database-connections/run-script', { conid: connection._id, database: name, sql });
const { errorMessage } = resp || {};
if (errorMessage) {
showModal(ErrorMessageModal, { title: 'Error when executing script', message: errorMessage });
} else {
showSnackbarSuccess('Saved to database');
}
}
const driver = findEngineDriver(connection, getExtensions());
const commands = _.flatten((apps || []).map(x => x.commands || []));
return [
{ onClick: handleNewQuery, text: 'New query', isNewQuery: true },
!driver?.dialect?.nosql && { onClick: handleNewTable, text: 'New table' },
@@ -180,6 +193,20 @@
_.get($currentDatabase, 'connection._id') == _.get(connection, '_id') &&
_.get($currentDatabase, 'name') == name && { onClick: handleDisconnect, text: 'Disconnect' },
commands.length > 0 && [
{ divider: true },
commands.map((cmd: any) => ({
text: cmd.name,
onClick: () => {
showModal(ConfirmSqlModal, {
sql: cmd.sql,
onConfirm: () => handleConfirmSql(cmd.sql),
engine: driver.engine,
});
},
})),
],
];
}
</script>
@@ -207,18 +234,22 @@
import { showSnackbarSuccess } from '../utility/snackbar';
import { findEngineDriver } from 'dbgate-tools';
import InputTextModal from '../modals/InputTextModal.svelte';
import { getDatabaseInfo } from '../utility/metadataLoaders';
import { getDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
import { openJsonDocument } from '../tabs/JsonTab.svelte';
import { apiCall } from '../utility/api';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
import { filterAppsForDatabase } from '../utility/appTools';
export let data;
export let passProps;
function createMenu() {
return getDatabaseMenuItems(data.connection, data.name, $extensions, $currentDatabase);
return getDatabaseMenuItems(data.connection, data.name, $extensions, $currentDatabase, $apps);
}
$: isPinned = !!$pinnedDatabases.find(x => x.name == data.name && x.connection?._id == data.connection?._id);
$: apps = useUsedApps();
</script>
<AppObjectCore

View File

@@ -30,7 +30,7 @@ import runCommand from './runCommand';
function themeCommand(theme: ThemeDefinition) {
return {
text: theme.themeName,
onClick: () => currentTheme.set(theme.className),
onClick: () => currentTheme.set(theme.themeClassName),
// onPreview: () => {
// const old = get(currentTheme);
// currentTheme.set(css);
@@ -128,6 +128,23 @@ registerCommand({
},
});
registerCommand({
id: 'new.application',
category: 'New',
icon: 'img app',
name: 'Application',
onClick: () => {
showModal(InputTextModal, {
value: '',
label: 'New application name',
header: 'Create application',
onConfirm: async folder => {
apiCall('apps/create-folder', { folder });
},
});
},
});
registerCommand({
id: 'new.table',
category: 'New',

View File

@@ -7,6 +7,8 @@
import { isTypeDateTime } from 'dbgate-tools';
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject.svelte';
import { copyTextToClipboard } from '../utility/clipboard';
import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
import { showModal } from '../modals/modalTools';
export let column;
export let conid = undefined;
@@ -14,6 +16,7 @@
export let setSort;
export let grouping = undefined;
export let order = undefined;
export let allowDefineVirtualReferences = false;
export let setGrouping;
const openReferencedTable = () => {
@@ -26,6 +29,16 @@
});
};
const handleDefineVirtualForeignKey = () => {
showModal(VirtualForeignKeyEditorModal, {
schemaName: column.schemaName,
pureName: column.pureName,
conid,
database,
columnName: column.columnName,
});
};
function getMenu() {
return [
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
@@ -49,6 +62,11 @@
{ onClick: () => setGrouping('GROUP:MONTH'), text: 'Group by MONTH' },
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
],
allowDefineVirtualReferences && [
{ divider: true },
{ onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
],
];
}
</script>

View File

@@ -303,6 +303,7 @@
export let errorMessage = undefined;
export let pureName = undefined;
export let schemaName = undefined;
export let allowDefineVirtualReferences = false;
export let isLoadedAll;
export let loadedTime;
@@ -1425,6 +1426,7 @@
}}
setGrouping={display.groupable ? groupFunc => display.setGrouping(col.uniqueName, groupFunc) : null}
grouping={display.getGrouping(col.uniqueName)}
{allowDefineVirtualReferences}
/>
</td>
{/each}

View File

@@ -7,7 +7,7 @@
TableGridDisplay,
} from 'dbgate-datalib';
import { getFilterValueExpression } from 'dbgate-filterparser';
import { findEngineDriver } from 'dbgate-tools';
import { extendDatabaseInfoFromApps, findEngineDriver } from 'dbgate-tools';
import _ from 'lodash';
import { writable } from 'svelte/store';
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
@@ -16,9 +16,11 @@
import {
useConnectionInfo,
useConnectionList,
useDatabaseInfo,
useDatabaseServerVersion,
useServerVersion,
useUsedApps,
} from '../utility/metadataLoaders';
import DataGrid from './DataGrid.svelte';
@@ -46,6 +48,9 @@
$: connection = useConnectionInfo({ conid });
$: dbinfo = useDatabaseInfo({ conid, database });
$: serverVersion = useDatabaseServerVersion({ conid, database });
$: apps = useUsedApps();
$: extendedDbInfo = extendDatabaseInfoFromApps($dbinfo, $apps);
$: connections = useConnectionList();
// $: console.log('serverVersion', $serverVersion);
@@ -64,10 +69,10 @@
setConfig,
cache,
setCache,
$dbinfo,
extendedDbInfo,
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
$serverVersion,
table => getDictionaryDescription(table, conid, database)
table => getDictionaryDescription(table, conid, database, $apps, $connections)
)
: null;
@@ -80,10 +85,10 @@
setConfig,
cache,
setCache,
$dbinfo,
extendedDbInfo,
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
$serverVersion,
table => getDictionaryDescription(table, conid, database)
table => getDictionaryDescription(table, conid, database, $apps, $connections)
)
: null;
@@ -159,6 +164,7 @@
macroCondition={macro => macro.type == 'transformValue'}
onReferenceSourceChanged={reference ? handleReferenceSourceChanged : null}
multipleGridsOnTab={multipleGridsOnTab || !!reference}
allowDefineVirtualReferences
onReferenceClick={value => {
if (value && value.referenceId && reference && reference.referenceId == value.referenceId) {
// reference not changed

View File

@@ -29,7 +29,7 @@
import DesignerTable from './DesignerTable.svelte';
import { isConnectedByReference } from './designerTools';
import uuidv1 from 'uuid/v1';
import { getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import { getTableInfo, useDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
import cleanupDesignColumns from './cleanupDesignColumns';
import _ from 'lodash';
import { writable } from 'svelte/store';
@@ -46,6 +46,7 @@
import { showModal } from '../modals/modalTools';
import ChooseColorModal from '../modals/ChooseColorModal.svelte';
import { currentThemeDefinition } from '../stores';
import { extendDatabaseInfoFromApps } from 'dbgate-tools';
export let value;
export let onChange;
@@ -67,10 +68,12 @@
const targetDragColumn$ = writable(null);
const dbInfo = settings?.updateFromDbInfo ? useDatabaseInfo({ conid, database }) : null;
$: dbInfoExtended = $dbInfo ? extendDatabaseInfoFromApps($dbInfo, $apps) : null;
$: tables = value?.tables as any[];
$: references = value?.references as any[];
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
$: apps = useUsedApps();
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;
@@ -94,8 +97,8 @@
}
$: {
if (dbInfo) {
updateFromDbInfo($dbInfo);
if (dbInfoExtended) {
updateFromDbInfo(dbInfoExtended as any);
}
}
@@ -104,13 +107,13 @@
}
$: {
if (dbInfo && value?.autoLayout) {
performAutoActions($dbInfo);
if (dbInfoExtended && value?.autoLayout) {
performAutoActions(dbInfoExtended);
}
}
function updateFromDbInfo(db = 'auto') {
if (db == 'auto' && dbInfo) db = $dbInfo;
if (db == 'auto' && dbInfo) db = dbInfoExtended as any;
if (!settings?.updateFromDbInfo || !db) return;
onChange(current => {
@@ -372,8 +375,8 @@
};
const handleAddTableReferences = async table => {
if (!dbInfo) return;
const db = $dbInfo;
if (!dbInfoExtended) return;
const db = dbInfoExtended;
if (!db) return;
callChange(current => {
return getTablesWithReferences(db, table, current);
@@ -692,13 +695,17 @@
if (css) css += '\n';
css += cssItem;
}
if ($currentThemeDefinition?.themeCss) {
if (css) css += '\n';
css += $currentThemeDefinition?.themeCss;
}
saveFileToDisk(async filePath => {
await apiCall('files/export-diagram', {
filePath,
html: domCanvas.outerHTML,
css,
themeType: $currentThemeDefinition?.themeType,
themeClassName: $currentThemeDefinition?.className,
themeClassName: $currentThemeDefinition?.themeClassName,
});
});
}

View File

@@ -9,6 +9,7 @@
import InputTextModal from '../modals/InputTextModal.svelte';
import { showModal } from '../modals/modalTools';
import { currentThemeDefinition } from '../stores';
import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
import contextMenu from '../utility/contextMenu';
import moveDrag from '../utility/moveDrag';
import ColumnLine from './ColumnLine.svelte';
@@ -166,6 +167,15 @@
});
};
const handleDefineVirtualForeignKey = table => {
showModal(VirtualForeignKeyEditorModal, {
schemaName: table.schemaName,
pureName: table.pureName,
conid,
database,
});
};
function createMenu() {
return [
{ text: 'Remove', onClick: () => onRemoveTable({ designerId }) },
@@ -185,6 +195,11 @@
settings?.allowAddAllReferences &&
!isMultipleTableSelection && { text: 'Add references', onClick: () => onAddAllReferences(table) },
settings?.allowChangeColor && { text: 'Change color', onClick: () => onChangeTableColor(table) },
settings?.allowDefineVirtualReferences &&
!isMultipleTableSelection && {
text: 'Define virtual foreign key',
onClick: () => handleDefineVirtualForeignKey(table),
},
settings?.appendTableSystemMenu &&
!isMultipleTableSelection && [{ divider: true }, createDatabaseObjectMenu({ ...table, conid, database })],
];

View File

@@ -21,6 +21,7 @@
allowChangeColor: true,
appendTableSystemMenu: true,
customizeStyle: true,
allowDefineVirtualReferences: true,
}}
referenceComponent={DiagramDesignerReference}
/>

View File

@@ -21,6 +21,7 @@
allowChangeColor: false,
appendTableSystemMenu: false,
customizeStyle: false,
allowDefineVirtualReferences: false,
}}
referenceComponent={QueryDesignerReference}
/>

View File

@@ -9,11 +9,13 @@
export let name;
export let options;
export let isClearable = false;
export let selectFieldComponent = SelectField;
const { values, setFieldValue } = getFormContext();
</script>
<SelectField
<svelte:component
this={selectFieldComponent}
{...$$restProps}
value={$values && $values[name]}
options={_.compact(options)}

View File

@@ -0,0 +1,48 @@
<script lang="ts">
import _ from 'lodash';
import { createEventDispatcher } from 'svelte';
import SelectField from '../forms/SelectField.svelte';
import { currentDatabase } from '../stores';
import { filterAppsForDatabase } from '../utility/appTools';
import { useAppFolders, useUsedApps } from '../utility/metadataLoaders';
export let value = '#new';
export let disableInitialize = false;
const dispatch = createEventDispatcher();
$: appFolders = useAppFolders();
$: usedApps = useUsedApps();
$: {
if (!disableInitialize && value == '#new' && $currentDatabase) {
const filtered = filterAppsForDatabase($currentDatabase.connection, $currentDatabase.name, $usedApps || []);
const common = _.intersection(
($appFolders || []).map(x => x.name),
filtered.map(x => x.name)
);
if (common.length > 0) {
value = common[0] as string;
dispatch('change', value);
}
}
}
</script>
<SelectField
isNative
{...$$restProps}
{value}
on:change={e => {
value = e.detail;
dispatch('change', value);
}}
options={[
{ label: '(New application linked to current DB)', value: '#new' },
...($appFolders || []).map(app => ({
label: app.name,
value: app.name,
})),
]}
/>

View File

@@ -26,6 +26,7 @@
'icon version': 'mdi mdi-ticket-confirmation',
'icon pin': 'mdi mdi-pin',
'icon arrange': 'mdi mdi-arrange-send-to-back',
'icon app': 'mdi mdi-layers-triple',
'icon columns': 'mdi mdi-view-column',
'icon columns-outline': 'mdi mdi-view-column-outline',
@@ -126,6 +127,9 @@
'img diagram': 'mdi mdi-graph color-icon-blue',
'img yaml': 'mdi mdi-code-brackets color-icon-red',
'img compare': 'mdi mdi-compare color-icon-red',
'img app': 'mdi mdi-layers-triple color-icon-magenta',
'img app-command': 'mdi mdi-flash color-icon-green',
'img app-query': 'mdi mdi-view-comfy color-icon-magenta',
'img add': 'mdi mdi-plus-circle color-icon-green',
'img minus': 'mdi mdi-minus-circle color-icon-red',

View File

@@ -1,10 +1,11 @@
<script lang="ts">
import FormProvider from '../forms/FormProvider.svelte';
import _ from 'lodash';
import FormSubmit from '../forms/FormSubmit.svelte';
import FormStyledButton from '../elements/FormStyledButton.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
import { useTableInfo } from '../utility/metadataLoaders';
import { useAppFolders, useConnectionList, useTableInfo, useUsedApps } from '../utility/metadataLoaders';
import TableControl from '../elements/TableControl.svelte';
import TextField from '../forms/TextField.svelte';
import FormTextField from '../forms/FormTextField.svelte';
@@ -19,6 +20,10 @@
} from '../utility/dictionaryDescriptionTools';
import { includes } from 'lodash';
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
import FormSelectField from '../forms/FormSelectField.svelte';
import TargetApplicationSelect from '../forms/TargetApplicationSelect.svelte';
import { currentDatabase } from '../stores';
import { filterAppsForDatabase } from '../utility/appTools';
export let conid;
export let database;
@@ -28,18 +33,41 @@
$: tableInfo = useTableInfo({ conid, database, schemaName, pureName });
$: descriptionInfo = getDictionaryDescription($tableInfo, conid, database, true);
$: apps = useUsedApps();
$: appFolders = useAppFolders();
$: connections = useConnectionList();
const values = writable({});
$: descriptionInfo = getDictionaryDescription($tableInfo, conid, database, $apps, $connections, true);
const values = writable({ targetApplication: '#new' } as any);
function initValues(descriptionInfo) {
$values = {
targetApplication: $values.targetApplication,
columns: descriptionInfo.expression,
delimiter: descriptionInfo.delimiter,
};
}
$: if (descriptionInfo) initValues(descriptionInfo);
$: {
if (descriptionInfo) initValues(descriptionInfo);
}
$: {
if ($values.targetApplication == '#new' && $currentDatabase) {
const filtered = filterAppsForDatabase($currentDatabase.connection, $currentDatabase.name, $apps || []);
const common = _.intersection(
($appFolders || []).map(x => x.name),
filtered.map(x => x.name)
);
if (common.length > 0) {
$values = {
...$values,
targetApplication: common[0],
};
}
}
}
</script>
<FormProviderCore {values}>
@@ -75,7 +103,14 @@
<FormTextField name="delimiter" label="Delimiter" />
<FormCheckboxField name="useForAllDatabases" label="Use for all databases" />
<FormSelectField
label="Target application"
name="targetApplication"
disableInitialize
selectFieldComponent={TargetApplicationSelect}
/>
<!-- <FormCheckboxField name="useForAllDatabases" label="Use for all databases" /> -->
<svelte:fragment slot="footer">
<FormSubmit
@@ -89,7 +124,7 @@
database,
$values.columns,
$values.delimiter,
$values.useForAllDatabases
$values.targetApplication
);
onConfirm();
}}

View File

@@ -6,7 +6,7 @@
import { closeCurrentModal, showModal } from './modalTools';
import DefineDictionaryDescriptionModal from './DefineDictionaryDescriptionModal.svelte';
import ScrollableTableControl from '../elements/ScrollableTableControl.svelte';
import { getTableInfo } from '../utility/metadataLoaders';
import { getTableInfo, useConnectionList, useUsedApps } from '../utility/metadataLoaders';
import { getDictionaryDescription } from '../utility/dictionaryDescriptionTools';
import { onMount } from 'svelte';
import { dumpSqlSelect } from 'dbgate-sqltree';
@@ -33,6 +33,9 @@
let checkedKeys = [];
$: apps = useUsedApps();
$: connections = useConnectionList();
function defineDescription() {
showModal(DefineDictionaryDescriptionModal, {
conid,
@@ -45,7 +48,7 @@
async function reload() {
tableInfo = await getTableInfo({ conid, database, schemaName, pureName });
description = getDictionaryDescription(tableInfo, conid, database);
description = getDictionaryDescription(tableInfo, conid, database, $apps, $connections);
if (!tableInfo || !description) return;
if (tableInfo?.primaryKey?.columns?.length != 1) return;
@@ -112,6 +115,8 @@
$: {
search;
$apps;
$connections;
reload();
}

View File

@@ -1,5 +1,5 @@
<script context="module">
export const className = 'theme-dark';
export const themeClassName = 'theme-dark';
export const themeName = 'Dark';
export const themeType = 'dark';
</script>

View File

@@ -1,5 +1,5 @@
<script context="module">
export const className = 'theme-light';
export const themeClassName = 'theme-light';
export const themeName = 'Light';
export const themeType = 'light';
</script>

View File

@@ -64,6 +64,7 @@ export const openedModals = writable([]);
export const openedSnackbars = writable([]);
export const nullStore = readable(null, () => {});
export const currentArchive = writableWithStorage('default', 'currentArchive');
export const currentApplication = writableWithStorage(null, 'currentApplication');
export const isFileDragActive = writable(false);
export const selectedCellsCallback = writable(null);
export const loadingPluginStore = writable({
@@ -72,7 +73,7 @@ export const loadingPluginStore = writable({
});
export const currentThemeDefinition = derived([currentTheme, extensions], ([$currentTheme, $extensions]) =>
$extensions.themes.find(x => x.className == $currentTheme)
$extensions.themes.find(x => x.themeClassName == $currentTheme)
);
subscribeCssVariable(selectedWidget, x => (x ? 1 : 0), '--dim-visible-left-panel');

View File

@@ -81,7 +81,7 @@
value={fullNameToString({ pureName: refTableName, schemaName: refSchemaName })}
isNative
notSelected
options={(dbInfo?.tables || []).map(tbl => ({
options={_.sortBy(dbInfo?.tables || [], ['schemaName', 'pureName']).map(tbl => ({
label: fullNameToLabel(tbl),
value: fullNameToString(tbl),
}))}

View File

@@ -0,0 +1,190 @@
<script lang="ts">
import FormStyledButton from '../elements/FormStyledButton.svelte';
import FormProvider from '../forms/FormProvider.svelte';
import FormSubmit from '../forms/FormSubmit.svelte';
import ModalBase from '../modals/ModalBase.svelte';
import { closeCurrentModal } from '../modals/modalTools';
import { fullNameFromString, fullNameToLabel, fullNameToString } from 'dbgate-tools';
import SelectField from '../forms/SelectField.svelte';
import _ from 'lodash';
import { useDatabaseInfo, useTableInfo } from '../utility/metadataLoaders';
import { onMount } from 'svelte';
import TargetApplicationSelect from '../forms/TargetApplicationSelect.svelte';
import { apiCall } from '../utility/api';
import { saveDbToApp } from '../utility/appTools';
export let conid;
export let database;
export let schemaName;
export let pureName;
export let columnName;
let dstApp;
const dbInfo = useDatabaseInfo({ conid, database });
const tableInfo = useTableInfo({ conid, database, schemaName, pureName });
let columns = [];
let refTableName = null;
let refSchemaName = null;
$: refTableInfo = $dbInfo?.tables?.find(x => x.pureName == refTableName && x.schemaName == refSchemaName);
// $dbInfo?.views?.find(x => x.pureName == refTableName && x.schemaName == refSchemaName);
onMount(() => {
if (columnName) {
columns = [
...columns,
{
columnName,
},
];
}
});
</script>
<FormProvider>
<ModalBase {...$$restProps}>
<svelte:fragment slot="header">Virtual foreign key</svelte:fragment>
<div class="largeFormMarker">
<div class="row">
<div class="label col-3">Referenced table</div>
<div class="col-9">
<SelectField
value={fullNameToString({ pureName: refTableName, schemaName: refSchemaName })}
isNative
notSelected
options={[
..._.sortBy($dbInfo?.tables || [], ['schemaName', 'pureName']),
// ..._.sortBy($dbInfo?.views || [], ['schemaName', 'pureName']),
].map(tbl => ({
label: fullNameToLabel(tbl),
value: fullNameToString(tbl),
}))}
on:change={e => {
if (e.detail) {
const name = fullNameFromString(e.detail);
refTableName = name.pureName;
refSchemaName = name.schemaName;
}
}}
/>
</div>
</div>
<div class="row">
<div class="col-5 mr-1">
Base column - {$tableInfo.pureName}
</div>
<div class="col-5 ml-1">
Ref column - {refTableName || '(table not set)'}
</div>
</div>
{#each columns as column, index}
<div class="row">
<div class="col-5 mr-1">
{#key column.columnName}
<SelectField
value={column.columnName}
isNative
notSelected
options={$tableInfo.columns.map(col => ({
label: col.columnName,
value: col.columnName,
}))}
on:change={e => {
if (e.detail) {
columns = columns.map((col, i) => (i == index ? { ...col, columnName: e.detail } : col));
}
}}
/>
{/key}
</div>
<div class="col-5 ml-1">
{#key column.refColumnName}
<SelectField
value={column.refColumnName}
isNative
notSelected
options={(refTableInfo?.columns || []).map(col => ({
label: col.columnName,
value: col.columnName,
}))}
on:change={e => {
if (e.detail) {
columns = columns.map((col, i) => (i == index ? { ...col, refColumnName: e.detail } : col));
}
}}
/>
{/key}
</div>
<div class="col-2 button">
<FormStyledButton
value="Delete"
on:click={e => {
const x = [...columns];
x.splice(index, 1);
columns = x;
}}
/>
</div>
</div>
{/each}
<FormStyledButton
type="button"
value="Add column"
on:click={() => {
columns = [...columns, {}];
}}
/>
<div class="row">
<div class="label col-3">Target application</div>
<div class="col-9">
<TargetApplicationSelect bind:value={dstApp} />
</div>
</div>
</div>
<svelte:fragment slot="footer">
<FormSubmit
value={'Save'}
on:click={async () => {
const appFolder = await saveDbToApp(conid, database, dstApp);
await apiCall('apps/save-virtual-reference', {
appFolder,
schemaName,
pureName,
refSchemaName,
refTableName,
columns,
});
closeCurrentModal();
}}
/>
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />
</svelte:fragment>
</ModalBase>
</FormProvider>
<style>
.row {
margin: var(--dim-large-form-margin);
display: flex;
}
.row .label {
white-space: nowrap;
align-self: center;
}
.button {
align-self: center;
text-align: right;
}
</style>

View File

@@ -0,0 +1,83 @@
<script lang="ts" context="module">
const getCurrentEditor = () => getActiveComponent('JsonEditorTab');
registerFileCommands({
idPrefix: 'json',
category: 'Json',
getCurrentEditor,
folder: 'yaml',
format: 'text',
fileExtension: 'json',
toggleComment: true,
findReplace: true,
});
</script>
<script lang="ts">
import { getContext } from 'svelte';
import { registerFileCommands } from '../commands/stdCommands';
import AceEditor from '../query/AceEditor.svelte';
import useEditorData from '../query/useEditorData';
import invalidateCommands from '../commands/invalidateCommands';
import createActivator, { getActiveComponent } from '../utility/createActivator';
export let tabid;
const tabVisible: any = getContext('tabVisible');
export const activator = createActivator('JsonEditorTab', false);
let domEditor;
$: if ($tabVisible && domEditor) {
domEditor?.getEditor()?.focus();
}
export function getData() {
return $editorState.value || '';
}
export function toggleComment() {
domEditor.getEditor().execCommand('togglecomment');
}
export function find() {
domEditor.getEditor().execCommand('find');
}
export function replace() {
domEditor.getEditor().execCommand('replace');
}
export function getTabId() {
return tabid;
}
const { editorState, editorValue, setEditorData, saveToStorage } = useEditorData({ tabid });
function createMenu() {
return [
{ command: 'json.toggleComment' },
{ divider: true },
{ command: 'json.save' },
{ command: 'json.saveAs' },
{ divider: true },
{ command: 'json.find' },
{ command: 'json.replace' },
];
}
</script>
<AceEditor
value={$editorState.value || ''}
menu={createMenu()}
on:input={e => setEditorData(e.detail)}
on:focus={() => {
activator.activate();
invalidateCommands();
}}
bind:this={domEditor}
mode="json"
/>

View File

@@ -31,7 +31,7 @@
const tabVisible: any = getContext('tabVisible');
export const activator = createActivator('MarkdownEditorTab', false);
export const activator = createActivator('YamlEditorTab', false);
let domEditor;
@@ -63,8 +63,6 @@
function createMenu() {
return [
{ command: 'yaml.preview' },
{ divider: true },
{ command: 'yaml.toggleComment' },
{ divider: true },
{ command: 'yaml.save' },

View File

@@ -15,6 +15,7 @@ import * as FavoriteEditorTab from './FavoriteEditorTab.svelte';
import * as QueryDesignTab from './QueryDesignTab.svelte';
import * as CommandListTab from './CommandListTab.svelte';
import * as YamlEditorTab from './YamlEditorTab.svelte';
import * as JsonEditorTab from './JsonEditorTab.svelte';
import * as CompareModelTab from './CompareModelTab.svelte';
import * as JsonTab from './JsonTab.svelte';
import * as ChangelogTab from './ChangelogTab.svelte';
@@ -38,6 +39,7 @@ export default {
QueryDesignTab,
CommandListTab,
YamlEditorTab,
JsonEditorTab,
CompareModelTab,
JsonTab,
ChangelogTab,

View File

@@ -0,0 +1,33 @@
import { ApplicationDefinition, StoredConnection } from 'dbgate-types';
import { apiCall } from '../utility/api';
export async function saveDbToApp(conid: string, database: string, app: string) {
if (app == '#new') {
const folder = await apiCall('apps/create-folder', { folder: database });
await apiCall('connections/update-database', {
conid,
database,
values: {
[`useApp:${folder}`]: true,
},
});
return folder;
}
await apiCall('connections/update-database', {
conid,
database,
values: {
[`useApp:${app}`]: true,
},
});
return app;
}
export function filterAppsForDatabase(connection, database: string, $apps): ApplicationDefinition[] {
const db = (connection?.databases || []).find(x => x.name == database);
return $apps.filter(app => db && db[`useApp:${app.name}`]);
}

View File

@@ -1,7 +1,8 @@
import { DictionaryDescription } from 'dbgate-datalib';
import { TableInfo } from 'dbgate-types';
import { ApplicationDefinition, TableInfo } from 'dbgate-types';
import _ from 'lodash';
import { getLocalStorage, setLocalStorage, removeLocalStorage } from './storageCache';
import { apiCall } from './api';
import { filterAppsForDatabase, saveDbToApp } from './appTools';
function checkDescriptionColumns(columns: string[], table: TableInfo) {
if (!columns?.length) return false;
@@ -14,17 +15,20 @@ export function getDictionaryDescription(
table: TableInfo,
conid: string,
database: string,
apps: ApplicationDefinition[],
connections,
skipCheckSaved: boolean = false
): DictionaryDescription {
const keySpecific = `dictionary_spec_${table.schemaName}||${table.pureName}||${conid}||${database}`;
const keyCommon = `dictionary_spec_${table.schemaName}||${table.pureName}`;
const conn = connections.find(x => x._id == conid);
const dbApps = filterAppsForDatabase(conn, database, apps);
const cachedSpecific = getLocalStorage(keySpecific);
const cachedCommon = getLocalStorage(keyCommon);
const cached = _.flatten(dbApps.map(x => x.dictionaryDescriptions || [])).find(
x => x.pureName == table.pureName && x.schemaName == table.schemaName
);
if (cachedSpecific && (skipCheckSaved || checkDescriptionColumns(cachedSpecific.columns, table)))
return cachedSpecific;
if (cachedCommon && (skipCheckSaved || checkDescriptionColumns(cachedCommon.columns, table))) return cachedCommon;
if (cached && (skipCheckSaved || checkDescriptionColumns(cached.columns, table))) {
return cached;
}
const descColumn = table.columns.find(x => x?.dataType?.toLowerCase()?.includes('char'));
if (descColumn) {
@@ -57,29 +61,22 @@ export function changeDelimitedColumnList(columns, columnName, isChecked) {
return parsed.join(',');
}
export function saveDictionaryDescription(
export async function saveDictionaryDescription(
table: TableInfo,
conid: string,
database: string,
expression: string,
delimiter: string,
useForAllDatabases: boolean
targetApplication: string
) {
const keySpecific = `dictionary_spec_${table.schemaName}||${table.pureName}||${conid}||${database}`;
const keyCommon = `dictionary_spec_${table.schemaName}||${table.pureName}`;
const appFolder = await saveDbToApp(conid, database, targetApplication);
removeLocalStorage(keySpecific);
if (useForAllDatabases) removeLocalStorage(keyCommon);
const description = {
await apiCall('apps/save-dictionary-description', {
appFolder,
schemaName: table.schemaName,
pureName: table.pureName,
columns: parseDelimitedColumnList(expression),
expression,
delimiter,
};
if (useForAllDatabases) {
setLocalStorage(keyCommon, description);
} else {
setLocalStorage(keySpecific, description);
}
});
}

View File

@@ -103,6 +103,30 @@ const archiveFilesLoader = ({ folder }) => ({
reloadTrigger: `archive-files-changed-${folder}`,
});
const appFoldersLoader = () => ({
url: 'apps/folders',
params: {},
reloadTrigger: `app-folders-changed`,
});
const appFilesLoader = ({ folder }) => ({
url: 'apps/files',
params: { folder },
reloadTrigger: `app-files-changed-${folder}`,
});
// const dbAppsLoader = ({ conid, database }) => ({
// url: 'apps/get-apps-for-db',
// params: { conid, database },
// reloadTrigger: `db-apps-changed-${conid}-${database}`,
// });
const usedAppsLoader = ({ conid, database }) => ({
url: 'apps/get-used-apps',
params: { },
reloadTrigger: `used-apps-changed`,
});
const serverStatusLoader = () => ({
url: 'server-connections/server-status',
params: {},
@@ -401,6 +425,36 @@ export function useArchiveFolders(args = {}) {
return useCore(archiveFoldersLoader, args);
}
export function getAppFiles(args) {
return getCore(appFilesLoader, args);
}
export function useAppFiles(args) {
return useCore(appFilesLoader, args);
}
export function getAppFolders(args = {}) {
return getCore(appFoldersLoader, args);
}
export function useAppFolders(args = {}) {
return useCore(appFoldersLoader, args);
}
export function getUsedApps(args = {}) {
return getCore(usedAppsLoader, args);
}
export function useUsedApps(args = {}) {
return useCore(usedAppsLoader, args);
}
// export function getDbApps(args = {}) {
// return getCore(dbAppsLoader, args);
// }
// export function useDbApps(args = {}) {
// return useCore(dbAppsLoader, args);
// }
export function getInstalledPlugins(args = {}) {
return getCore(installedPluginsLoader, args) || [];
}

View File

@@ -0,0 +1,101 @@
<script lang="ts" context="module">
const APP_LABELS = {
'command.sql': 'SQL commands',
'query.sql': 'SQL queries',
};
const COMMAND_TEMPLATE = `-- Write SQL command here
-- After save, you can execute it from database context menu, for all databases, which use this application
`;
const QUERY_TEMPLATE = `-- Write SQL query here
-- After save, you can view it in tables list, for all databases, which use this application
`;
</script>
<script lang="ts">
import { createFreeTableModel } from 'dbgate-datalib';
import _ from 'lodash';
import AppObjectList from '../appobj/AppObjectList.svelte';
import * as appFileAppObject from '../appobj/AppFileAppObject.svelte';
import CloseSearchButton from '../elements/CloseSearchButton.svelte';
import DropDownButton from '../elements/DropDownButton.svelte';
import InlineButton from '../elements/InlineButton.svelte';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import InputTextModal from '../modals/InputTextModal.svelte';
import { showModal } from '../modals/modalTools';
import newQuery from '../query/newQuery';
import { currentApplication } from '../stores';
import { apiCall } from '../utility/api';
import { markArchiveFileAsDataSheet } from '../utility/archiveTools';
import { useAppFiles, useArchiveFolders } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
let filter = '';
$: folder = $currentApplication;
$: files = useAppFiles({ folder });
const handleRefreshFiles = () => {
apiCall('apps/refresh-files', { folder });
};
function handleNewSqlFile(fileType, header, initialData) {
showModal(InputTextModal, {
value: '',
label: 'New file name',
header,
onConfirm: async file => {
newQuery({
title: file,
initialData,
// @ts-ignore
savedFile: file + '.' + fileType,
savedFolder: 'app:' + $currentApplication,
savedFormat: 'text',
appFolder: $currentApplication,
});
},
});
}
function createAddMenu() {
return [
{
text: 'New SQL command',
onClick: () => handleNewSqlFile('command.sql', 'Create new SQL command', COMMAND_TEMPLATE),
},
// { text: 'New query view', onClick: () => handleNewSqlFile('query.sql', 'Create new SQL query', QUERY_TEMPLATE) },
];
}
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search application files" bind:value={filter} />
<CloseSearchButton bind:filter />
<DropDownButton icon="icon plus-thick" menu={createAddMenu} />
<InlineButton on:click={handleRefreshFiles} title="Refresh files of selected application">
<FontIcon icon="icon refresh" />
</InlineButton>
</SearchBoxWrapper>
<WidgetsInnerContainer>
<AppObjectList
list={($files || []).map(file => ({
fileName: file.name,
folderName: folder,
fileType: file.type,
fileLabel: file.label,
}))}
groupFunc={data => APP_LABELS[data.fileType] || 'App config'}
module={appFileAppObject}
{filter}
/>
</WidgetsInnerContainer>

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import _ from 'lodash';
import AppObjectList from '../appobj/AppObjectList.svelte';
import * as appFolderAppObject from '../appobj/AppFolderAppObject.svelte';
import runCommand from '../commands/runCommand';
import CloseSearchButton from '../elements/CloseSearchButton.svelte';
import InlineButton from '../elements/InlineButton.svelte';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import { apiCall } from '../utility/api';
import { useAppFolders } from '../utility/metadataLoaders';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
let filter = '';
$: folders = useAppFolders();
const handleRefreshFolders = () => {
apiCall('apps/refresh-folders');
};
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search applications" bind:value={filter} />
<CloseSearchButton bind:filter />
<InlineButton on:click={() => runCommand('new.application')} title="Create new application">
<FontIcon icon="icon plus-thick" />
</InlineButton>
<InlineButton on:click={handleRefreshFolders} title="Refresh application list">
<FontIcon icon="icon refresh" />
</InlineButton>
</SearchBoxWrapper>
<WidgetsInnerContainer>
<AppObjectList list={_.sortBy($folders, 'name')} module={appFolderAppObject} {filter} />
</WidgetsInnerContainer>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import AppFilesList from './AppFilesList.svelte';
import WidgetColumnBar from './WidgetColumnBar.svelte';
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
import { useFavorites } from '../utility/metadataLoaders';
import AppFolderList from './AppFolderList.svelte';
</script>
<WidgetColumnBar>
<WidgetColumnBarItem title="Applications" name="apps" height="30%" storageName="appsWidget">
<AppFolderList />
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Application files" name="files" storageName="appFilesWidget">
<AppFilesList />
</WidgetColumnBarItem>
</WidgetColumnBar>

View File

@@ -6,6 +6,7 @@
import PluginsWidget from './PluginsWidget.svelte';
import CellDataWidget from './CellDataWidget.svelte';
import HistoryWidget from './HistoryWidget.svelte';
import AppWidget from './AppWidget.svelte';
</script>
<DatabaseWidget hidden={$selectedWidget != 'database'} />
@@ -25,3 +26,6 @@
{#if $selectedWidget == 'cell-data'}
<CellDataWidget />
{/if}
{#if $selectedWidget == 'app'}
<AppWidget />
{/if}

View File

@@ -40,6 +40,11 @@
name: 'cell-data',
title: 'Selected cell data detail view',
},
{
icon: 'icon app',
name: 'app',
title: 'Application layers',
},
// {
// icon: 'icon settings',
// name: 'settings',