SYNC: Merge pull request #9 from dbgate/feature/apps

This commit is contained in:
Jan Prochazka
2025-09-11 13:10:36 +02:00
committed by Diflow
parent ef15f299d2
commit 11a4f0ef32
40 changed files with 1770 additions and 754 deletions

View File

@@ -20,7 +20,7 @@
installNewVolatileConnectionListener,
refreshPublicCloudFiles,
} from './utility/api';
import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders';
import { getAllApps, getConfig, getSettings } from './utility/metadataLoaders';
import AppTitleProvider from './utility/AppTitleProvider.svelte';
import getElectron from './utility/getElectron';
import AppStartInfo from './widgets/AppStartInfo.svelte';
@@ -49,7 +49,7 @@
const connections = await apiCall('connections/list');
const settings = await getSettings();
const apps = await getUsedApps();
const apps = await getAllApps();
const loadedApiValue = !!(settings && connections && config && apps);
if (loadedApiValue) {

View File

@@ -0,0 +1,35 @@
<script lang='ts'>
import PermissionCheckBox from './PermissionCheckBox.svelte';
import { getFormContext } from '../forms/FormProviderCore.svelte';
const { values } = getFormContext();
export let onSetPermission;
export let label;
export let folder;
</script>
<PermissionCheckBox
{label}
permission={`files/${folder}/*`}
permissions={$values.permissions}
basePermissions={$values.basePermissions}
{onSetPermission}
/>
<div class="ml-4">
<PermissionCheckBox
label="Read"
permission={`files/${folder}/read`}
permissions={$values.permissions}
basePermissions={$values.basePermissions}
{onSetPermission}
/>
<PermissionCheckBox
label="Write"
permission={`files/${folder}/write`}
permissions={$values.permissions}
basePermissions={$values.basePermissions}
{onSetPermission}
/>
</div>

View File

@@ -36,6 +36,7 @@
export let filter = null;
export let disableHover = false;
export let divProps = {};
export let additionalIcons = null;
$: isChecked =
checkedObjectsStore && $checkedObjectsStore.find(x => module?.extractKey(data) == module?.extractKey(x));
@@ -160,6 +161,11 @@
/>
</span>
{/if}
{#if additionalIcons}
{#each additionalIcons as ic}
<FontIcon icon={ic.icon} title={ic.title} colorClass={ic.colorClass} />
{/each}
{/if}
{#if extInfo}
<span class="ext-info">
<TokenizedFilteredText text={extInfo} {filter} />

View File

@@ -130,7 +130,7 @@
import openNewTab from '../utility/openNewTab';
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
import getElectron from '../utility/getElectron';
import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders';
import { getDatabaseList, useAllApps } from '../utility/metadataLoaders';
import { getLocalStorage } from '../utility/storageCache';
import { apiCall, removeVolatileMapping } from '../utility/api';
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
@@ -383,7 +383,7 @@
$currentDatabase,
$apps,
$openedSingleDatabaseConnections,
data.databasePermissionRole,
data.databasePermissionRole
),
],
@@ -427,7 +427,7 @@
}
}
$: apps = useUsedApps();
$: apps = useAllApps();
</script>
<AppObjectCore

View File

@@ -405,9 +405,25 @@ await dbgateApi.executeQuery(${JSON.stringify(
});
};
const handleCreateNewApp = () => {
showModal(InputTextModal, {
header: 'New application',
label: 'Application name',
value: _.startCase(name),
onConfirm: async appName => {
const newAppId = await apiCall('apps/create-app-from-db', {
appName,
server: connection?.server,
database: name,
});
openApplicationEditor(newAppId);
},
});
};
const driver = findEngineDriver(connection, getExtensions());
const commands = _.flatten((apps || []).map(x => x.commands || []));
const commands = _.flatten((apps || []).map(x => Object.values(x.files || {}).filter(x => x.type == 'command')));
const isSqlOrDoc =
driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document');
@@ -564,11 +580,26 @@ await dbgateApi.executeQuery(${JSON.stringify(
text: _t('database.dataDeployer', { defaultMessage: 'Data deployer' }),
},
isProApp() &&
hasPermission(`files/apps/write`) && {
onClick: handleCreateNewApp,
text: _t('database.createNewApplication', { defaultMessage: 'Create new application' }),
},
isProApp() &&
apps?.length > 0 && {
text: _t('database.editApplications', { defaultMessage: 'Edit application' }),
submenu: apps.map((app: any) => ({
text: app.applicationName,
onClick: () => openApplicationEditor(app.appid),
})),
},
{ divider: true },
commands.length > 0 && [
commands.map((cmd: any) => ({
text: cmd.name,
text: cmd.label,
onClick: () => {
showModal(ConfirmSqlModal, {
sql: cmd.sql,
@@ -618,12 +649,12 @@ await dbgateApi.executeQuery(${JSON.stringify(
getConnectionLabel,
} from 'dbgate-tools';
import InputTextModal from '../modals/InputTextModal.svelte';
import { getDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
import { getDatabaseInfo, useAllApps, useDatabaseInfoPeek } from '../utility/metadataLoaders';
import { openJsonDocument } from '../tabs/JsonTab.svelte';
import { apiCall } from '../utility/api';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import ConfirmSqlModal, { runOperationOnDatabase, saveScriptToDatabase } from '../modals/ConfirmSqlModal.svelte';
import { filterAppsForDatabase } from '../utility/appTools';
import { filterAppsForDatabase, openApplicationEditor } from '../utility/appTools';
import newQuery from '../query/newQuery';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
@@ -639,7 +670,7 @@ await dbgateApi.executeQuery(${JSON.stringify(
import { getNumberIcon } from '../icons/FontIcon.svelte';
import { getDatabaseClickActionSetting } from '../settings/settingsTools';
import { _t } from '../translations';
import { dataGridRowHeight } from '../datagrid/DataGridRowHeightMeter.svelte';
import { tick } from 'svelte';
export let data;
export let passProps;
@@ -657,8 +688,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
}
$: isPinned = !!$pinnedDatabases.find(x => x?.name == data.name && x?.connection?._id == data.connection?._id);
$: apps = useUsedApps();
$: apps = useAllApps();
$: isLoadingSchemas = $loadingSchemaLists[`${data?.connection?._id}::${data?.name}`];
$: dbInfo = useDatabaseInfoPeek({ conid: data?.connection?._id, database: data?.name });
$: appsForDb = filterAppsForDatabase(data?.connection, data?.name, $apps, $dbInfo);
// $: console.log('AppsForDB:', data?.name, appsForDb);
</script>
<AppObjectCore
@@ -681,6 +717,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
switchCurrentDatabase(data);
}
}}
additionalIcons={appsForDb?.length > 0
? appsForDb.map(ic => ({
icon: ic.applicationIcon || 'img app',
title: ic.applicationName,
colorClass: ic.applicationColor ? `color-icon-${ic.applicationColor}` : undefined,
}))
: null}
on:mousedown={() => {
$focusedConnectionOrDatabase = { conid: data.connection?._id, database: data.name, connection: data.connection };
}}

View File

@@ -45,16 +45,16 @@
schedulerEvents: 'icon scheduler-event',
};
const defaultTabs = {
tables: 'TableDataTab',
collections: 'CollectionDataTab',
views: 'ViewDataTab',
matviews: 'ViewDataTab',
queries: 'QueryDataTab',
procedures: 'SqlObjectTab',
functions: 'SqlObjectTab',
triggers: 'SqlObjectTab',
};
// const defaultTabs = {
// tables: 'TableDataTab',
// collections: 'CollectionDataTab',
// views: 'ViewDataTab',
// matviews: 'ViewDataTab',
// queries: 'QueryDataTab',
// procedures: 'SqlObjectTab',
// functions: 'SqlObjectTab',
// triggers: 'SqlObjectTab',
// };
function createScriptTemplatesSubmenu(objectTypeField) {
return {
@@ -724,7 +724,7 @@
});
const filteredNoEmptySubmenus = filteredSumenus.filter(x => !x.submenu || x.submenu.length > 0);
return filteredNoEmptySubmenus;
}
@@ -741,7 +741,7 @@
export async function openDatabaseObjectDetail(
tabComponent,
scriptTemplate,
{ schemaName, pureName, conid, database, objectTypeField, defaultActionId, isRawMode },
{ schemaName, pureName, conid, database, objectTypeField, defaultActionId, isRawMode, sql },
forceNewTab?,
initialData?,
icon?,
@@ -776,6 +776,7 @@
initialArgs: scriptTemplate ? { scriptTemplate } : null,
defaultActionId,
isRawMode,
sql,
},
},
initialData,
@@ -797,7 +798,7 @@
data,
{ forceNewTab = false, tabPreviewMode = false, focusTab = false } = {}
) {
const { schemaName, pureName, conid, database, objectTypeField } = data;
const { schemaName, pureName, conid, database, objectTypeField, sql } = data;
const driver = findEngineDriver(data, getExtensions());
const activeTab = getActiveTab();
@@ -843,6 +844,7 @@
objectTypeField,
defaultActionId: prefferedAction.defaultActionId,
isRawMode: prefferedAction?.isRawMode ?? false,
sql,
},
forceNewTab,
prefferedAction?.initialData,

View File

@@ -142,6 +142,18 @@
label: 'Model transform file',
};
const apps: FileTypeHandler = isProApp()
? {
icon: 'img app',
format: 'json',
tabComponent: 'AppEditorTab',
folder: 'apps',
currentConnection: false,
extension: 'json',
label: 'Application file',
}
: undefined;
export const SAVED_FILE_HANDLERS = {
sql,
shell,
@@ -154,6 +166,7 @@
modtrans,
datadeploy,
dbcompare,
apps,
};
export const extractKey = data => data.file;

View File

@@ -100,4 +100,12 @@ export const defaultDatabaseObjectAppObjectActions = {
icon: 'img sql-file',
},
],
queries: [
{
label: 'Show query',
tab: 'QueryDataTab',
defaultActionId: 'showAppQuery',
icon: 'img app-query',
},
],
};

View File

@@ -268,6 +268,23 @@ if (isProApp()) {
});
}
if (isProApp()) {
registerCommand({
id: 'new.application',
category: 'New',
icon: 'img app',
name: 'Application',
menuName: 'New application',
onClick: () => {
openNewTab({
title: 'Application #',
icon: 'img app',
tabComponent: 'AppEditorTab',
});
},
});
}
registerCommand({
id: 'new.diagram',
category: 'New',
@@ -297,22 +314,22 @@ 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.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',

View File

@@ -10,6 +10,8 @@
import { copyTextToClipboard } from '../utility/clipboard';
import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
import { showModal } from '../modals/modalTools';
import DefineDictionaryDescriptionModal from '../modals/DefineDictionaryDescriptionModal.svelte';
import { sleep } from '../utility/common';
export let column;
export let conid = undefined;
@@ -24,6 +26,7 @@
export let allowDefineVirtualReferences = false;
export let setGrouping;
export let seachInColumns = '';
export let onReload = undefined;
const openReferencedTable = () => {
openDatabaseObjectDetail('TableDataTab', null, {
@@ -45,6 +48,19 @@
});
};
const handleCustomizeDescriptions = () => {
showModal(DefineDictionaryDescriptionModal, {
conid,
database,
schemaName: column.foreignKey.refSchemaName,
pureName: column.foreignKey.refTableName,
onConfirm: async () => {
await sleep(100);
onReload?.();
},
});
};
function getMenu() {
return [
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
@@ -72,10 +88,13 @@
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
],
allowDefineVirtualReferences && [
{ divider: true },
{ onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
],
{ divider: true },
allowDefineVirtualReferences && { onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
column.foreignKey && {
onClick: handleCustomizeDescriptions,
text: 'Customize description',
},
];
}
</script>

View File

@@ -2003,6 +2003,7 @@
grouping={display.getGrouping(col.uniqueName)}
{allowDefineVirtualReferences}
seachInColumns={display.config?.searchInColumns}
onReload={refresh}
/>
</td>
{/each}

View File

@@ -15,13 +15,13 @@
import stableStringify from 'json-stable-stringify';
import {
useAllApps,
useConnectionInfo,
useConnectionList,
useDatabaseInfo,
useDatabaseServerVersion,
useServerVersion,
useSettings,
useUsedApps,
} from '../utility/metadataLoaders';
import DataGrid from './DataGrid.svelte';
@@ -53,7 +53,7 @@
$: connection = useConnectionInfo({ conid });
$: dbinfo = useDatabaseInfo({ conid, database });
$: serverVersion = useDatabaseServerVersion({ conid, database });
$: apps = useUsedApps();
$: apps = useAllApps();
$: extendedDbInfo = extendDatabaseInfoFromApps($dbinfo, $apps);
$: connections = useConnectionList();
const settingsValue = useSettings();

View File

@@ -42,7 +42,7 @@
import DesignerTable from './DesignerTable.svelte';
import { isConnectedByReference } from './designerTools';
import uuidv1 from 'uuid/v1';
import { getTableInfo, useDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
import { getTableInfo, useAllApps, useDatabaseInfo } from '../utility/metadataLoaders';
import cleanupDesignColumns from './cleanupDesignColumns';
import _ from 'lodash';
import { writable } from 'svelte/store';
@@ -108,7 +108,7 @@
ref => tables.find(x => x.designerId == ref.sourceId) && tables.find(x => x.designerId == ref.targetId)
) as any[];
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
$: apps = useUsedApps();
$: apps = useAllApps();
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;

View File

@@ -0,0 +1,444 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import FontIcon from '../icons/FontIcon.svelte';
import InlineButton from '../buttons/InlineButton.svelte';
import { getFormContext } from './FormProviderCore.svelte';
export let name;
export let label;
export let defaultIcon;
export let templateProps = {};
const { template, values, setFieldValue } = getFormContext();
let showPicker = false;
// Real-world subject icons for application identification
const ICONS = [
{ icon: defaultIcon, name: '(Default icon)' },
// Applications & Tools
{ icon: 'mdi mdi-application', name: 'Application' },
{ icon: 'mdi mdi-cog', name: 'Settings' },
{ icon: 'mdi mdi-tools', name: 'Tools' },
{ icon: 'mdi mdi-wrench', name: 'Wrench' },
{ icon: 'mdi mdi-hammer', name: 'Hammer' },
{ icon: 'mdi mdi-screwdriver', name: 'Screwdriver' },
{ icon: 'mdi mdi-palette', name: 'Palette' },
{ icon: 'mdi mdi-brush', name: 'Brush' },
{ icon: 'mdi mdi-calculator', name: 'Calculator' },
// Files & Folders
{ icon: 'mdi mdi-file', name: 'File' },
{ icon: 'mdi mdi-folder', name: 'Folder' },
{ icon: 'mdi mdi-folder-open', name: 'Folder Open' },
{ icon: 'mdi mdi-file-document', name: 'Document' },
{ icon: 'mdi mdi-file-image', name: 'Image File' },
{ icon: 'mdi mdi-file-video', name: 'Video File' },
{ icon: 'mdi mdi-file-music', name: 'Music File' },
{ icon: 'mdi mdi-archive', name: 'Archive' },
// Core Applications
{ icon: 'mdi mdi-database', name: 'Database' },
{ icon: 'mdi mdi-server', name: 'Server' },
{ icon: 'mdi mdi-web', name: 'Web' },
{ icon: 'mdi mdi-cloud', name: 'Cloud' },
{ icon: 'mdi mdi-monitor', name: 'Monitor' },
{ icon: 'mdi mdi-laptop', name: 'Laptop' },
{ icon: 'mdi mdi-cellphone', name: 'Mobile' },
// Business & Finance
{ icon: 'mdi mdi-briefcase', name: 'Business' },
{ icon: 'mdi mdi-bank', name: 'Banking' },
{ icon: 'mdi mdi-currency-usd', name: 'Finance' },
{ icon: 'mdi mdi-chart-line', name: 'Analytics' },
{ icon: 'mdi mdi-chart-bar', name: 'Reports' },
{ icon: 'mdi mdi-chart-pie', name: 'Statistics' },
{ icon: 'mdi mdi-calculator', name: 'Calculator' },
{ icon: 'mdi mdi-cash-register', name: 'Sales' },
{ icon: 'mdi mdi-credit-card', name: 'Payments' },
{ icon: 'mdi mdi-receipt', name: 'Invoicing' },
// Communication & Social
{ icon: 'mdi mdi-email', name: 'Email' },
{ icon: 'mdi mdi-phone', name: 'Phone' },
{ icon: 'mdi mdi-message', name: 'Messaging' },
{ icon: 'mdi mdi-chat', name: 'Chat' },
{ icon: 'mdi mdi-forum', name: 'Forum' },
{ icon: 'mdi mdi-account-group', name: 'Team' },
{ icon: 'mdi mdi-bullhorn', name: 'Marketing' },
{ icon: 'mdi mdi-newspaper', name: 'News' },
// Education & Knowledge
{ icon: 'mdi mdi-school', name: 'Education' },
{ icon: 'mdi mdi-book', name: 'Library' },
{ icon: 'mdi mdi-book-open', name: 'Learning' },
{ icon: 'mdi mdi-certificate', name: 'Certification' },
{ icon: 'mdi mdi-graduation-cap', name: 'Academic' },
{ icon: 'mdi mdi-microscope', name: 'Research' },
{ icon: 'mdi mdi-flask', name: 'Laboratory' },
{ icon: 'mdi mdi-library', name: 'Archive' },
// Healthcare & Medical
{ icon: 'mdi mdi-hospital-building', name: 'Hospital' },
{ icon: 'mdi mdi-medical-bag', name: 'Medical' },
{ icon: 'mdi mdi-heart-pulse', name: 'Health' },
{ icon: 'mdi mdi-pill', name: 'Pharmacy' },
{ icon: 'mdi mdi-tooth', name: 'Dental' },
{ icon: 'mdi mdi-eye', name: 'Vision' },
{ icon: 'mdi mdi-stethoscope', name: 'Clinic' },
// Transportation & Logistics
{ icon: 'mdi mdi-truck', name: 'Logistics' },
{ icon: 'mdi mdi-car', name: 'Automotive' },
{ icon: 'mdi mdi-airplane', name: 'Aviation' },
{ icon: 'mdi mdi-ship-wheel', name: 'Maritime' },
{ icon: 'mdi mdi-train', name: 'Railway' },
{ icon: 'mdi mdi-bus', name: 'Transit' },
{ icon: 'mdi mdi-bike', name: 'Cycling' },
{ icon: 'mdi mdi-map', name: 'Navigation' },
{ icon: 'mdi mdi-gas-station', name: 'Fuel' },
// Real Estate & Construction
{ icon: 'mdi mdi-home', name: 'Real Estate' },
{ icon: 'mdi mdi-office-building', name: 'Commercial' },
{ icon: 'mdi mdi-factory', name: 'Industrial' },
{ icon: 'mdi mdi-hammer', name: 'Construction' },
{ icon: 'mdi mdi-wrench', name: 'Maintenance' },
{ icon: 'mdi mdi-tools', name: 'Tools' },
{ icon: 'mdi mdi-city', name: 'Urban Planning' },
// Retail & E-commerce
{ icon: 'mdi mdi-store', name: 'Retail' },
{ icon: 'mdi mdi-shopping', name: 'Shopping' },
{ icon: 'mdi mdi-cart', name: 'E-commerce' },
{ icon: 'mdi mdi-barcode', name: 'Inventory' },
{ icon: 'mdi mdi-package-variant', name: 'Shipping' },
{ icon: 'mdi mdi-gift', name: 'Gifts' },
// Entertainment & Media
{ icon: 'mdi mdi-camera', name: 'Photography' },
{ icon: 'mdi mdi-video', name: 'Video' },
{ icon: 'mdi mdi-music', name: 'Music' },
{ icon: 'mdi mdi-gamepad-variant', name: 'Gaming' },
{ icon: 'mdi mdi-movie', name: 'Cinema' },
{ icon: 'mdi mdi-television', name: 'Broadcasting' },
{ icon: 'mdi mdi-radio', name: 'Radio' },
{ icon: 'mdi mdi-theater', name: 'Theater' },
// Food & Hospitality
{ icon: 'mdi mdi-food', name: 'Food Service' },
{ icon: 'mdi mdi-coffee', name: 'Cafe' },
{ icon: 'mdi mdi-silverware-fork-knife', name: 'Restaurant' },
{ icon: 'mdi mdi-pizza', name: 'Pizza' },
{ icon: 'mdi mdi-cake', name: 'Bakery' },
{ icon: 'mdi mdi-glass-wine', name: 'Bar' },
{ icon: 'mdi mdi-bed', name: 'Hotel' },
// Sports & Fitness
{ icon: 'mdi mdi-dumbbell', name: 'Fitness' },
{ icon: 'mdi mdi-basketball', name: 'Basketball' },
{ icon: 'mdi mdi-soccer', name: 'Soccer' },
{ icon: 'mdi mdi-tennis', name: 'Tennis' },
{ icon: 'mdi mdi-golf', name: 'Golf' },
{ icon: 'mdi mdi-run', name: 'Running' },
{ icon: 'mdi mdi-swim', name: 'Swimming' },
{ icon: 'mdi mdi-yoga', name: 'Yoga' },
// Nature & Environment
{ icon: 'mdi mdi-tree', name: 'Forestry' },
{ icon: 'mdi mdi-flower', name: 'Gardening' },
{ icon: 'mdi mdi-leaf', name: 'Environment' },
{ icon: 'mdi mdi-weather-sunny', name: 'Weather' },
{ icon: 'mdi mdi-earth', name: 'Geography' },
{ icon: 'mdi mdi-water', name: 'Water' },
{ icon: 'mdi mdi-fire', name: 'Energy' },
{ icon: 'mdi mdi-lightning-bolt', name: 'Power' },
// Science & Technology
{ icon: 'mdi mdi-rocket', name: 'Aerospace' },
{ icon: 'mdi mdi-atom', name: 'Physics' },
{ icon: 'mdi mdi-dna', name: 'Genetics' },
{ icon: 'mdi mdi-telescope', name: 'Astronomy' },
{ icon: 'mdi mdi-robot', name: 'Robotics' },
{ icon: 'mdi mdi-chip', name: 'Electronics' },
// Security & Safety
{ icon: 'mdi mdi-shield', name: 'Security' },
{ icon: 'mdi mdi-lock', name: 'Access Control' },
{ icon: 'mdi mdi-key', name: 'Authentication' },
{ icon: 'mdi mdi-fire-truck', name: 'Emergency' },
{ icon: 'mdi mdi-police-badge', name: 'Law Enforcement' },
// Time & Scheduling
{ icon: 'mdi mdi-calendar', name: 'Calendar' },
{ icon: 'mdi mdi-clock', name: 'Time Tracking' },
{ icon: 'mdi mdi-timer', name: 'Timer' },
{ icon: 'mdi mdi-alarm', name: 'Reminders' },
// Creative & Design
{ icon: 'mdi mdi-palette', name: 'Design' },
{ icon: 'mdi mdi-brush', name: 'Art' },
{ icon: 'mdi mdi-draw', name: 'Drawing' },
{ icon: 'mdi mdi-image', name: 'Graphics' },
{ icon: 'mdi mdi-format-paint', name: 'Painting' },
// Alpha Icons
{ icon: 'mdi mdi-alpha-a-circle', name: 'A' },
{ icon: 'mdi mdi-alpha-b-circle', name: 'B' },
{ icon: 'mdi mdi-alpha-c-circle', name: 'C' },
{ icon: 'mdi mdi-alpha-d-circle', name: 'D' },
{ icon: 'mdi mdi-alpha-e-circle', name: 'E' },
{ icon: 'mdi mdi-alpha-f-circle', name: 'F' },
{ icon: 'mdi mdi-alpha-g-circle', name: 'G' },
{ icon: 'mdi mdi-alpha-h-circle', name: 'H' },
{ icon: 'mdi mdi-alpha-i-circle', name: 'I' },
{ icon: 'mdi mdi-alpha-j-circle', name: 'J' },
{ icon: 'mdi mdi-alpha-k-circle', name: 'K' },
{ icon: 'mdi mdi-alpha-l-circle', name: 'L' },
{ icon: 'mdi mdi-alpha-m-circle', name: 'M' },
{ icon: 'mdi mdi-alpha-n-circle', name: 'N' },
{ icon: 'mdi mdi-alpha-o-circle', name: 'O' },
{ icon: 'mdi mdi-alpha-p-circle', name: 'P' },
{ icon: 'mdi mdi-alpha-q-circle', name: 'Q' },
{ icon: 'mdi mdi-alpha-r-circle', name: 'R' },
{ icon: 'mdi mdi-alpha-s-circle', name: 'S' },
{ icon: 'mdi mdi-alpha-t-circle', name: 'T' },
{ icon: 'mdi mdi-alpha-u-circle', name: 'U' },
{ icon: 'mdi mdi-alpha-v-circle', name: 'V' },
{ icon: 'mdi mdi-alpha-w-circle', name: 'W' },
{ icon: 'mdi mdi-alpha-x-circle', name: 'X' },
{ icon: 'mdi mdi-alpha-y-circle', name: 'Y' },
{ icon: 'mdi mdi-alpha-z-circle', name: 'Z' },
// Numeric Icons
{ icon: 'mdi mdi-numeric-0-circle', name: '0' },
{ icon: 'mdi mdi-numeric-1-circle', name: '1' },
{ icon: 'mdi mdi-numeric-2-circle', name: '2' },
{ icon: 'mdi mdi-numeric-3-circle', name: '3' },
{ icon: 'mdi mdi-numeric-4-circle', name: '4' },
{ icon: 'mdi mdi-numeric-5-circle', name: '5' },
{ icon: 'mdi mdi-numeric-6-circle', name: '6' },
{ icon: 'mdi mdi-numeric-7-circle', name: '7' },
{ icon: 'mdi mdi-numeric-8-circle', name: '8' },
{ icon: 'mdi mdi-numeric-9-circle', name: '9' },
{ icon: 'mdi mdi-numeric-10-circle', name: '10' },
// Alpha Outline Icons
{ icon: 'mdi mdi-alpha-a-circle-outline', name: 'A Outline' },
{ icon: 'mdi mdi-alpha-b-circle-outline', name: 'B Outline' },
{ icon: 'mdi mdi-alpha-c-circle-outline', name: 'C Outline' },
{ icon: 'mdi mdi-alpha-d-circle-outline', name: 'D Outline' },
{ icon: 'mdi mdi-alpha-e-circle-outline', name: 'E Outline' },
{ icon: 'mdi mdi-alpha-f-circle-outline', name: 'F Outline' },
{ icon: 'mdi mdi-alpha-g-circle-outline', name: 'G Outline' },
{ icon: 'mdi mdi-alpha-h-circle-outline', name: 'H Outline' },
{ icon: 'mdi mdi-alpha-i-circle-outline', name: 'I Outline' },
{ icon: 'mdi mdi-alpha-j-circle-outline', name: 'J Outline' },
{ icon: 'mdi mdi-alpha-k-circle-outline', name: 'K Outline' },
{ icon: 'mdi mdi-alpha-l-circle-outline', name: 'L Outline' },
{ icon: 'mdi mdi-alpha-m-circle-outline', name: 'M Outline' },
{ icon: 'mdi mdi-alpha-n-circle-outline', name: 'N Outline' },
{ icon: 'mdi mdi-alpha-o-circle-outline', name: 'O Outline' },
{ icon: 'mdi mdi-alpha-p-circle-outline', name: 'P Outline' },
{ icon: 'mdi mdi-alpha-q-circle-outline', name: 'Q Outline' },
{ icon: 'mdi mdi-alpha-r-circle-outline', name: 'R Outline' },
{ icon: 'mdi mdi-alpha-s-circle-outline', name: 'S Outline' },
{ icon: 'mdi mdi-alpha-t-circle-outline', name: 'T Outline' },
{ icon: 'mdi mdi-alpha-u-circle-outline', name: 'U Outline' },
{ icon: 'mdi mdi-alpha-v-circle-outline', name: 'V Outline' },
{ icon: 'mdi mdi-alpha-w-circle-outline', name: 'W Outline' },
{ icon: 'mdi mdi-alpha-x-circle-outline', name: 'X Outline' },
{ icon: 'mdi mdi-alpha-y-circle-outline', name: 'Y Outline' },
{ icon: 'mdi mdi-alpha-z-circle-outline', name: 'Z Outline' },
// Numeric Outline Icons
{ icon: 'mdi mdi-numeric-0-circle-outline', name: '0 Outline' },
{ icon: 'mdi mdi-numeric-1-circle-outline', name: '1 Outline' },
{ icon: 'mdi mdi-numeric-2-circle-outline', name: '2 Outline' },
{ icon: 'mdi mdi-numeric-3-circle-outline', name: '3 Outline' },
{ icon: 'mdi mdi-numeric-4-circle-outline', name: '4 Outline' },
{ icon: 'mdi mdi-numeric-5-circle-outline', name: '5 Outline' },
{ icon: 'mdi mdi-numeric-6-circle-outline', name: '6 Outline' },
{ icon: 'mdi mdi-numeric-7-circle-outline', name: '7 Outline' },
{ icon: 'mdi mdi-numeric-8-circle-outline', name: '8 Outline' },
{ icon: 'mdi mdi-numeric-9-circle-outline', name: '9 Outline' },
{ icon: 'mdi mdi-numeric-10-circle-outline', name: '10 Outline' },
];
function selectIcon(iconName) {
setFieldValue(name, iconName);
showPicker = false;
}
function togglePicker() {
showPicker = !showPicker;
}
function handleKeydown(event, action) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
action();
}
}
$: iconValue = $values?.[name];
</script>
<svelte:component this={template} type="select" {label} {...templateProps}>
<div class="icon-field-container">
<div
class="selected-icon"
on:click={togglePicker}
on:keydown={e => handleKeydown(e, togglePicker)}
role="button"
tabindex="0"
>
<FontIcon icon={iconValue || defaultIcon} />
<span class="icon-name">{ICONS.find(icon => icon.icon === iconValue)?.name || '(Default icon)'}</span>
<FontIcon icon="icon chevron-down" />
</div>
{#if showPicker}
<div class="icon-picker">
<div class="icon-picker-header">
<span>Choose an icon</span>
<InlineButton on:click={togglePicker}>
<FontIcon icon="icon close" />
</InlineButton>
</div>
<div class="icon-grid">
{#each ICONS as { icon, name: iconDisplayName }}
<div
class="icon-option"
class:selected={iconValue === icon}
on:click={() => selectIcon(icon)}
on:keydown={e => handleKeydown(e, () => selectIcon(icon))}
role="button"
tabindex="0"
title={iconDisplayName}
>
<FontIcon {icon} />
<span class="icon-label">{iconDisplayName}</span>
</div>
{/each}
</div>
</div>
{/if}
</div>
</svelte:component>
<style>
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--theme-font-1);
}
.icon-field-container {
position: relative;
}
.selected-icon {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border: 1px solid var(--theme-border);
border-radius: 4px;
background: var(--theme-bg-0);
cursor: pointer;
transition: border-color 0.2s;
}
.selected-icon:hover {
border-color: var(--theme-border-hover);
}
.selected-icon:focus {
outline: none;
border-color: var(--theme-font-link);
box-shadow: 0 0 0 2px var(--theme-font-link-opacity);
}
.icon-name {
flex: 1;
color: var(--theme-font-1);
}
.icon-picker {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: var(--theme-bg-0);
border: 1px solid var(--theme-border);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-height: 400px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.icon-picker-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem;
border-bottom: 1px solid var(--theme-border);
background: var(--theme-bg-1);
font-weight: 500;
}
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 1px;
padding: 0.5rem;
overflow-y: auto;
max-height: 320px;
}
.icon-option {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.5rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
text-align: center;
}
.icon-option:hover {
background: var(--theme-bg-hover);
}
.icon-option.selected {
background: var(--theme-bg-selected);
color: var(--theme-font-link);
}
.icon-option:focus {
outline: none;
background: var(--theme-bg-hover);
box-shadow: 0 0 0 2px var(--theme-font-link-opacity);
}
.icon-label {
font-size: 0.75rem;
color: var(--theme-font-2);
line-height: 1.2;
word-break: break-word;
}
.icon-option.selected .icon-label {
color: var(--theme-font-link);
font-weight: 500;
}
</style>

View File

@@ -11,7 +11,7 @@
<TextField
{...$$restProps}
value={$values[name] ?? defaultValue}
value={$values?.[name] ?? defaultValue}
on:input={e => setFieldValue(name, e.target['value'])}
on:input={e => {
if (saveOnInput) {

View File

@@ -1,48 +1,78 @@
<script lang="ts">
import _ from 'lodash';
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, tick } from 'svelte';
import SelectField from '../forms/SelectField.svelte';
import { currentDatabase } from '../stores';
import { filterAppsForDatabase } from '../utility/appTools';
import { useAppFolders, useUsedApps } from '../utility/metadataLoaders';
import { getConnectionInfo, useAllApps, useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import InlineButton from '../buttons/InlineButton.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import { showModal } from '../modals/modalTools';
import InputTextModal from '../modals/InputTextModal.svelte';
import { apiCall } from '../utility/api';
export let value = '#new';
export let disableInitialize = false;
export let value = '';
export let conid;
export let database;
const dispatch = createEventDispatcher();
let selectFieldKey = 0;
$: appFolders = useAppFolders();
$: usedApps = useUsedApps();
$: dbInfo = useDatabaseInfo({ conid, database });
$: connectionInfo = useConnectionInfo({ conid });
$: {
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;
$: allApps = useAllApps();
$: apps = filterAppsForDatabase($connectionInfo, database, $allApps || [], $dbInfo);
$: if (apps?.length == 1) {
value = apps[0].appid;
selectFieldKey++;
dispatch('change', value);
}
async function handleAddNewApplication() {
showModal(InputTextModal, {
header: 'New application',
label: 'Application name',
value: _.startCase(database),
onConfirm: async appName => {
const newAppId = await apiCall('apps/create-app-from-db', {
appName,
server: $connectionInfo?.server,
database,
});
await tick();
value = newAppId;
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,
})),
]}
/>
<div class="flex">
{#key selectFieldKey}
<SelectField
isNative
{...$$restProps}
{value}
on:change={e => {
value = e.detail;
dispatch('change', value);
}}
options={[
{
label: '(not selected)',
value: '',
},
...(apps || []).map(app => ({
label: app.applicationName,
value: app.appid,
})),
]}
/>
{/key}
<InlineButton on:click={handleAddNewApplication} square>
<FontIcon icon="icon plus-thick" padLeft padRight />
</InlineButton>
</div>

View File

@@ -76,6 +76,7 @@
'icon send': 'mdi mdi-send',
'icon regex': 'mdi mdi-regex',
'icon list': 'mdi mdi-format-list-bulleted-triangle',
'icon help': 'mdi mdi-help',
'icon window-restore': 'mdi mdi-window-restore',
'icon window-maximize': 'mdi mdi-window-maximize',

View File

@@ -1,11 +1,10 @@
<script lang="ts">
import FormProvider from '../forms/FormProvider.svelte';
import _ from 'lodash';
import FormSubmit from '../forms/FormSubmit.svelte';
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
import { useAppFolders, useConnectionList, useTableInfo, useUsedApps } from '../utility/metadataLoaders';
import { useAllApps, useConnectionList, useTableInfo } from '../utility/metadataLoaders';
import TableControl from '../elements/TableControl.svelte';
import TextField from '../forms/TextField.svelte';
import FormTextField from '../forms/FormTextField.svelte';
@@ -16,14 +15,12 @@
checkDescriptionExpression,
getDictionaryDescription,
parseDelimitedColumnList,
saveDictionaryDescription,
} 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';
import { apiCall } from '../utility/api';
export let conid;
export let database;
@@ -33,13 +30,12 @@
$: tableInfo = useTableInfo({ conid, database, schemaName, pureName });
$: apps = useUsedApps();
$: appFolders = useAppFolders();
$: apps = useAllApps();
$: connections = useConnectionList();
$: descriptionInfo = getDictionaryDescription($tableInfo, conid, database, $apps, $connections, true);
const values = writable({ targetApplication: '#new' } as any);
const values = writable({ targetApplication: '' } as any);
function initValues(descriptionInfo) {
$values = {
@@ -52,28 +48,21 @@
$: {
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}>
<ModalBase {...$$restProps}>
<svelte:fragment slot="header">Define description</svelte:fragment>
<FormSelectField
label="Target application (mandatory)"
name="targetApplication"
disableInitialize
selectFieldComponent={TargetApplicationSelect}
{conid}
{database}
/>
<div class="wrapper">
<TableControl
rows={$tableInfo?.columns || []}
@@ -103,30 +92,34 @@
<FormTextField name="delimiter" label="Delimiter" />
<FormSelectField
label="Target application"
name="targetApplication"
disableInitialize
selectFieldComponent={TargetApplicationSelect}
/>
<!-- <FormCheckboxField name="useForAllDatabases" label="Use for all databases" /> -->
<svelte:fragment slot="footer">
<FormSubmit
value="OK"
disabled={!checkDescriptionExpression($values?.columns, $tableInfo)}
on:click={() => {
disabled={!checkDescriptionExpression($values?.columns, $tableInfo) || !$values.targetApplication}
on:click={async () => {
closeCurrentModal();
saveDictionaryDescription(
$tableInfo,
conid,
database,
$values.columns,
$values.delimiter,
$values.targetApplication
);
onConfirm();
const expression = $values.columns;
await apiCall('apps/save-dictionary-description', {
appid: $values.targetApplication,
schemaName: $tableInfo.schemaName,
pureName: $tableInfo.pureName,
columns: parseDelimitedColumnList(expression),
expression,
delimiter: $values.delimiter,
});
// saveDictionaryDescription(
// $tableInfo,
// conid,
// database,
// $values.columns,
// $values.delimiter,
// $values.targetApplication
// );
onConfirm?.();
}}
/>
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />

View File

@@ -6,7 +6,7 @@
import { closeCurrentModal, showModal } from './modalTools';
import DefineDictionaryDescriptionModal from './DefineDictionaryDescriptionModal.svelte';
import ScrollableTableControl from '../elements/ScrollableTableControl.svelte';
import { getTableInfo, useConnectionList, useUsedApps } from '../utility/metadataLoaders';
import { getTableInfo, useAllApps, useConnectionList } from '../utility/metadataLoaders';
import { getDictionaryDescription } from '../utility/dictionaryDescriptionTools';
import { onMount } from 'svelte';
import { dumpSqlSelect } from 'dbgate-sqltree';
@@ -34,7 +34,7 @@
let checkedKeys = [];
$: apps = useUsedApps();
$: apps = useAllApps();
$: connections = useConnectionList();
function defineDescription() {

View File

@@ -12,7 +12,8 @@
import { onMount, tick } from 'svelte';
import TargetApplicationSelect from '../forms/TargetApplicationSelect.svelte';
import { apiCall } from '../utility/api';
import { saveDbToApp } from '../utility/appTools';
// import { apiCall } from '../utility/api';
// import { saveDbToApp } from '../utility/appTools';
export let conid;
export let database;
@@ -173,7 +174,7 @@
<div class="row">
<div class="label col-3">Target application</div>
<div class="col-9">
<TargetApplicationSelect bind:value={dstApp} />
<TargetApplicationSelect bind:value={dstApp} {conid} {database} />
</div>
</div>
</div>
@@ -181,10 +182,10 @@
<svelte:fragment slot="footer">
<FormSubmit
value={'Save'}
disabled={!dstApp}
on:click={async () => {
const appFolder = await saveDbToApp(conid, database, dstApp);
await apiCall('apps/save-virtual-reference', {
appFolder,
appid: dstApp,
schemaName,
pureName,
refSchemaName,

View File

@@ -28,6 +28,10 @@
import { apiCall, apiOff, apiOn } from '../utility/api';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import useEffect from '../utility/useEffect';
import { getSqlFrontMatter } from 'dbgate-tools';
import yaml from 'js-yaml';
import JslChart from '../charts/JslChart.svelte';
import ToolStripButton from '../buttons/ToolStripButton.svelte';
export const activator = createActivator('QueryDataTab', true);
@@ -40,6 +44,8 @@
let jslid;
let loading = false;
$: frontMatter = getSqlFrontMatter(sql, yaml);
async function loadData(conid, database, sql) {
const resp = await apiCall('sessions/execute-reader', {
conid,
@@ -96,17 +102,30 @@
}
}
$: $effect;
$: selectedChart = frontMatter?.['selected-chart'];
$: fixedChartDefinition = selectedChart && frontMatter ? frontMatter?.[`chart-${selectedChart}`] : null;
</script>
<ToolStripContainer>
{#if jslid}
<JslDataGrid {jslid} listenInitializeFile onCustomGridRefresh={handleRefresh} focusOnVisible />
{:else}
{#if loading}
<LoadingInfo message="Loading data..." />
{:else if jslid}
{#if fixedChartDefinition}
<JslChart {jslid} fixedDefinition={fixedChartDefinition} />
{:else}
<JslDataGrid {jslid} listenInitializeFile onCustomGridRefresh={handleRefresh} focusOnVisible />
{/if}
{/if}
<svelte:fragment slot="toolstrip">
<ToolStripCommandButton command="dataGrid.refresh" />
{#if fixedChartDefinition}
<ToolStripButton on:click={handleRefresh} icon="icon refresh">Refresh</ToolStripButton>
{:else}
<ToolStripCommandButton command="dataGrid.refresh" />
{/if}
<ToolStripCommandButton command="queryData.stopLoading" />
<ToolStripExportButton command="jslTableGrid.export" {quickExportHandlerRef} />
{#if !fixedChartDefinition}
<ToolStripExportButton command="jslTableGrid.export" {quickExportHandlerRef} />
{/if}
</svelte:fragment>
</ToolStripContainer>

View File

@@ -1,33 +1,131 @@
import type { ApplicationDefinition, StoredConnection } from 'dbgate-types';
import type { ApplicationDefinition, DatabaseInfo, StoredConnection } from 'dbgate-types';
import { apiCall } from '../utility/api';
import _ from 'lodash';
import { match } from 'fuzzy';
import { getConnectionInfo } from './metadataLoaders';
import openNewTab from './openNewTab';
export async function saveDbToApp(conid: string, database: string, app: string) {
if (app == '#new') {
const folder = await apiCall('apps/create-folder', { folder: database });
// export async function saveDbToApp(conid: string, database: string, app: string) {
// const connection = await getConnectionInfo({ conid });
await apiCall('connections/update-database', {
conid,
database,
values: {
[`useApp:${folder}`]: true,
},
});
// if (app == '#new') {
// const appJson = {
// applicationName: _.startCase(database),
// usageRules: [
// {
// serverHostsList: connection?.server ? [connection.server] : undefined,
// databaseNamesList: [database],
// conditionGroup: '1',
// },
// ],
// };
return folder;
// const file =
// 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[],
dbinfo: DatabaseInfo = null
): ApplicationDefinition[] {
if (!apps) {
return [];
}
await apiCall('connections/update-database', {
conid,
database,
values: {
[`useApp:${app}`]: true,
},
// console.log('ALL APPS:', apps);
// console.log('DB INFO:', dbinfo);
// console.log('CONNECTION:', connection);
// console.log('DATABASE:', database);
return apps.filter(app => {
const groupedConditions = _.groupBy(app.usageRules, rule => rule.conditionGroup || '1');
for (const group of Object.values(groupedConditions)) {
let groupMatch = true;
for (const rule of group) {
let ruleMatch = true;
if (rule.serverHostsRegex) {
const re = new RegExp(rule.serverHostsRegex);
ruleMatch = ruleMatch && !!connection?.server && re.test(connection.server);
}
if (rule.serverHostsList) {
ruleMatch = ruleMatch && !!connection?.server && rule.serverHostsList.includes(connection.server);
}
if (rule.databaseNamesRegex) {
const re = new RegExp(rule.databaseNamesRegex);
ruleMatch = ruleMatch && !!database && re.test(database);
}
if (rule.databaseNamesList) {
ruleMatch = ruleMatch && !!database && rule.databaseNamesList.includes(database);
}
let matchedTables = dbinfo?.tables;
if (rule.tableNamesRegex) {
const re = new RegExp(rule.tableNamesRegex);
matchedTables = dbinfo?.tables?.filter(table => !!table && re.test(table.pureName)) || [];
ruleMatch = ruleMatch && matchedTables.length > 0;
}
if (rule.tableNamesList) {
matchedTables =
dbinfo?.tables?.filter(table => !!table && rule.tableNamesList.includes(table.pureName)) || [];
ruleMatch = ruleMatch && matchedTables.length > 0;
}
if (rule.columnNamesRegex) {
const re = new RegExp(rule.columnNamesRegex);
ruleMatch =
ruleMatch &&
matchedTables.some(table => !!table?.columns?.some(column => !!column && re.test(column.columnName)));
}
if (rule.columnNamesList) {
ruleMatch =
ruleMatch &&
matchedTables.some(
table => !!table?.columns?.some(column => !!column && rule.columnNamesList.includes(column.columnName))
);
}
groupMatch = groupMatch && ruleMatch;
}
if (groupMatch) return true;
}
return false;
});
return app;
// const db = (connection?.databases || []).find(x => x.name == database);
// return apps?.filter(app => db && db[`useApp:${app.name}`]);
}
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}`]);
export async function openApplicationEditor(appid) {
const dataContent = await apiCall('files/load', { folder: 'apps', file: appid, format: 'json' });
openNewTab(
{
title: appid,
icon: 'img app',
tabComponent: 'AppEditorTab',
props: {
savedFile: appid,
savedFolder: 'apps',
savedFormat: 'json',
},
},
{ editor: dataContent }
);
}

View File

@@ -6,6 +6,7 @@ const cachedByKey = {};
const cachedPromisesByKey = {};
const cachedKeysByReloadTrigger = {};
const subscriptionsByReloadTrigger = {};
const subscriptionsByByCacheKeyPeek = {};
const cacheGenerationByKey = {};
let cacheGeneration = 0;
@@ -29,6 +30,7 @@ function cacheSet(cacheKey, value, reloadTrigger, generation) {
addCacheKeyToReloadTrigger(cacheKey, reloadTrigger);
delete cachedPromisesByKey[cacheKey];
cacheGenerationByKey[cacheKey] = generation;
dispatchCacheChangePeek(cacheKey);
}
function cacheClean(reloadTrigger) {
@@ -64,6 +66,10 @@ function getCacheGenerationForKey(cacheKey) {
return cacheGenerationByKey[cacheKey] || 0;
}
export function getCachedValue(cacheKey) {
return cacheGet(cacheKey);
}
export async function loadCachedValue(reloadTrigger, cacheKey, func) {
const fromCache = cacheGet(cacheKey);
if (fromCache) {
@@ -107,12 +113,36 @@ export async function unsubscribeCacheChange(reloadTrigger, cacheKey, reloadHand
x => x != reloadHandler
);
}
if (subscriptionsByReloadTrigger[itemString].length == 0) {
if (subscriptionsByReloadTrigger[itemString]?.length == 0) {
delete subscriptionsByReloadTrigger[itemString];
}
}
}
export function subscribeCachePeek(cacheKey, peekHandler) {
if (!subscriptionsByByCacheKeyPeek[cacheKey]) {
subscriptionsByByCacheKeyPeek[cacheKey] = [];
}
subscriptionsByByCacheKeyPeek[cacheKey].push(peekHandler);
}
export function unsubscribeCachePeek(cacheKey, peekHandler) {
if (subscriptionsByByCacheKeyPeek[cacheKey]) {
subscriptionsByByCacheKeyPeek[cacheKey] = subscriptionsByByCacheKeyPeek[cacheKey].filter(x => x != peekHandler);
}
if (subscriptionsByByCacheKeyPeek[cacheKey]?.length == 0) {
delete subscriptionsByByCacheKeyPeek[cacheKey];
}
}
function dispatchCacheChangePeek(cacheKey) {
if (subscriptionsByByCacheKeyPeek[cacheKey]) {
for (const handler of subscriptionsByByCacheKeyPeek[cacheKey]) {
handler();
}
}
}
export function dispatchCacheChange(reloadTrigger) {
cacheClean(reloadTrigger);

View File

@@ -1,8 +1,9 @@
import type { DictionaryDescription } from 'dbgate-datalib';
import type { ApplicationDefinition, TableInfo } from 'dbgate-types';
import type { ApplicationDefinition, DatabaseInfo, TableInfo } from 'dbgate-types';
import _ from 'lodash';
import { apiCall } from './api';
import { filterAppsForDatabase, saveDbToApp } from './appTools';
import { filterAppsForDatabase } from './appTools';
// import { filterAppsForDatabase, saveDbToApp } from './appTools';
function checkDescriptionColumns(columns: string[], table: TableInfo) {
if (!columns?.length) return false;
@@ -17,7 +18,8 @@ export function getDictionaryDescription(
database: string,
apps: ApplicationDefinition[],
connections,
skipCheckSaved: boolean = false
skipCheckSaved: boolean = false,
dbInfo: DatabaseInfo = null
): DictionaryDescription {
const conn = connections?.find(x => x._id == conid);
@@ -25,7 +27,7 @@ export function getDictionaryDescription(
return null;
}
const dbApps = filterAppsForDatabase(conn, database, apps);
const dbApps = filterAppsForDatabase(conn, database, apps, dbInfo);
if (!dbApps) {
return null;
@@ -70,22 +72,20 @@ export function changeDelimitedColumnList(columns, columnName, isChecked) {
return parsed.join(',');
}
export async function saveDictionaryDescription(
table: TableInfo,
conid: string,
database: string,
expression: string,
delimiter: string,
targetApplication: string
) {
const appFolder = await saveDbToApp(conid, database, targetApplication);
await apiCall('apps/save-dictionary-description', {
appFolder,
schemaName: table.schemaName,
pureName: table.pureName,
columns: parseDelimitedColumnList(expression),
expression,
delimiter,
});
}
// export async function saveDictionaryDescription(
// table: TableInfo,
// conid: string,
// database: string,
// expression: string,
// delimiter: string,
// targetApplication: string
// ) {
// await apiCall('apps/save-dictionary-description', {
// appFolder,
// schemaName: table.schemaName,
// pureName: table.pureName,
// columns: parseDelimitedColumnList(expression),
// expression,
// delimiter,
// });
// }

View File

@@ -1,5 +1,12 @@
import _ from 'lodash';
import { loadCachedValue, subscribeCacheChange, unsubscribeCacheChange } from './cache';
import {
getCachedValue,
loadCachedValue,
subscribeCacheChange,
subscribeCachePeek,
unsubscribeCacheChange,
unsubscribeCachePeek,
} from './cache';
import stableStringify from 'json-stable-stringify';
import { derived } from 'svelte/store';
import { extendDatabaseInfo } from 'dbgate-tools';
@@ -107,17 +114,17 @@ const archiveFilesLoader = ({ folder }) => ({
reloadTrigger: { key: `archive-files-changed`, folder },
});
const appFoldersLoader = () => ({
url: 'apps/folders',
params: {},
reloadTrigger: { key: `app-folders-changed` },
});
// const appFoldersLoader = () => ({
// url: 'apps/folders',
// params: {},
// reloadTrigger: { key: `app-folders-changed` },
// });
const appFilesLoader = ({ folder }) => ({
url: 'apps/files',
params: { folder },
reloadTrigger: { key: `app-files-changed`, app: folder },
});
// const appFilesLoader = ({ folder }) => ({
// url: 'apps/files',
// params: { folder },
// reloadTrigger: { key: `app-files-changed`, app: folder },
// });
// const dbAppsLoader = ({ conid, database }) => ({
// url: 'apps/get-apps-for-db',
@@ -125,10 +132,10 @@ const appFilesLoader = ({ folder }) => ({
// reloadTrigger: `db-apps-changed-${conid}-${database}`,
// });
const usedAppsLoader = ({ conid, database }) => ({
url: 'apps/get-used-apps',
const allAppsLoader = () => ({
url: 'apps/get-all-apps',
params: {},
reloadTrigger: { key: `used-apps-changed` },
reloadTrigger: { key: `files-changed`, folder: 'apps' },
});
const serverStatusLoader = () => ({
@@ -227,6 +234,37 @@ function useCore(loader, args) {
};
}
function useCorePeek(loader, args) {
const { url, params, reloadTrigger, transform, onLoaded } = loader(args);
const cacheKey = stableStringify({ url, ...params });
let openedCount = 0;
return {
subscribe: onChange => {
async function handlePeek() {
const res = getCachedValue(cacheKey);
if (openedCount > 0) {
onChange(res);
}
}
openedCount += 1;
handlePeek();
if (reloadTrigger) {
subscribeCachePeek(cacheKey, handlePeek);
return () => {
openedCount -= 1;
unsubscribeCachePeek(cacheKey, handlePeek);
};
} else {
return () => {
openedCount -= 1;
};
}
},
};
}
/** @returns {Promise<import('dbgate-types').DatabaseInfo>} */
export function getDatabaseInfo(args) {
return getCore(databaseInfoLoader, args);
@@ -237,6 +275,10 @@ export function useDatabaseInfo(args) {
return useCore(databaseInfoLoader, args);
}
export function useDatabaseInfoPeek(args) {
return useCorePeek(databaseInfoLoader, args);
}
export async function getDbCore(args, objectTypeField = undefined) {
const db = await getDatabaseInfo(args);
if (!db) return null;
@@ -392,25 +434,25 @@ 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 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 getAppFolders(args = {}) {
// return getCore(appFoldersLoader, args);
// }
// export function useAppFolders(args = {}) {
// return useCore(appFoldersLoader, args);
// }
export function getUsedApps(args = {}) {
return getCore(usedAppsLoader, args);
export function getAllApps(args = {}) {
return getCore(allAppsLoader, args);
}
export function useUsedApps(args = {}) {
return useCore(usedAppsLoader, args);
export function useAllApps(args = {}) {
return useCore(allAppsLoader, args);
}
// export function getDbApps(args = {}) {

View File

@@ -1,120 +0,0 @@
<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 '../buttons/CloseSearchButton.svelte';
import DropDownButton from '../buttons/DropDownButton.svelte';
import InlineButton from '../buttons/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 { useAppFiles, useArchiveFolders } from '../utility/metadataLoaders';
import openNewTab from '../utility/openNewTab';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
import { showSnackbarError } from '../utility/snackbar';
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,
});
},
});
}
async function handleNewConfigFile(fileName, content) {
if (!(await apiCall('apps/create-config-file', { fileName, content, appFolder: $currentApplication }))) {
showSnackbarError('File not created, probably already exists');
}
}
function createAddMenu() {
return [
{
text: 'New SQL command',
onClick: () => handleNewSqlFile('command.sql', 'Create new SQL command', COMMAND_TEMPLATE),
},
{
text: 'New SQL query',
onClick: () => handleNewSqlFile('query.sql', 'Create new SQL query', QUERY_TEMPLATE),
},
{
text: 'New virtual references file',
onClick: () => handleNewConfigFile('virtual-references.config.json', []),
},
{
text: 'New dictionary descriptions file',
onClick: () => handleNewConfigFile('dictionary-descriptions.config.json', []),
},
// { 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

@@ -1,39 +0,0 @@
<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 '../buttons/CloseSearchButton.svelte';
import InlineButton from '../buttons/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

@@ -1,19 +0,0 @@
<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

@@ -12,6 +12,7 @@
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
import { isProApp } from '../utility/proTools';
import InlineUploadButton from '../buttons/InlineUploadButton.svelte';
import { DATA_FOLDER_NAMES } from 'dbgate-tools';
let filter = '';
@@ -27,6 +28,7 @@
const dbCompareJobFiles = useFiles({ folder: 'dbcompare' });
const perspectiveFiles = useFiles({ folder: 'perspectives' });
const modelTransformFiles = useFiles({ folder: 'modtrans' });
const appFiles = useFiles({ folder: 'apps' });
$: files = [
...($sqlFiles || []),
@@ -41,32 +43,18 @@
...($modelTransformFiles || []),
...((isProApp() && $dataDeployJobFiles) || []),
...((isProApp() && $dbCompareJobFiles) || []),
...((isProApp() && $appFiles) || []),
];
function handleRefreshFiles() {
apiCall('files/refresh', {
folders: [
'sql',
'shell',
'markdown',
'charts',
'query',
'sqlite',
'diagrams',
'perspectives',
'impexp',
'modtrans',
'datadeploy',
'dbcompare',
],
folders: DATA_FOLDER_NAMES.map(folder => folder.name),
});
}
function dataFolderTitle(folder) {
if (folder == 'modtrans') return 'Model transforms';
if (folder == 'datadeploy') return 'Data deploy jobs';
if (folder == 'dbcompare') return 'Database compare jobs';
return _.startCase(folder);
const foundFolder = DATA_FOLDER_NAMES.find(f => f.name === folder);
return foundFolder ? foundFolder.label : _.startCase(folder);
}
async function handleUploadedFile(filePath, fileName) {

View File

@@ -17,11 +17,11 @@
import SearchInput from '../elements/SearchInput.svelte';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
import {
useAllApps,
useConnectionInfo,
useDatabaseInfo,
useDatabaseStatus,
useSchemaList,
useUsedApps,
} from '../utility/metadataLoaders';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import AppObjectList from '../appobj/AppObjectList.svelte';
@@ -73,9 +73,8 @@
$: connection = useConnectionInfo({ conid });
$: driver = findEngineDriver($connection, $extensions);
$: apps = useUsedApps();
$: dbApps = filterAppsForDatabase($currentDatabase?.connection, $currentDatabase?.name, $apps || []);
$: apps = useAllApps();
$: appsForDb = filterAppsForDatabase($connection, database, $apps || [], $objects);
// $: console.log('OBJECTS', $objects);
@@ -87,13 +86,14 @@
['schemaName', 'pureName']
)
),
...dbApps.map(app =>
app.queries.map(query => ({
objectTypeField: 'queries',
pureName: query.name,
schemaName: app.name,
sql: query.sql,
}))
...appsForDb.map(app =>
Object.values(app.files || {})
.filter(x => x.type == 'query')
.map(query => ({
objectTypeField: 'queries',
pureName: query.label,
sql: query.sql,
}))
),
]);
@@ -281,7 +281,7 @@
>
<AppObjectList
list={objectList
.filter(x => ($appliedCurrentSchema ? x.schemaName == $appliedCurrentSchema : true))
.filter(x => x.schemaName == null || ($appliedCurrentSchema ? x.schemaName == $appliedCurrentSchema : true))
.map(x => ({ ...x, conid, database }))}
module={databaseObjectAppObject}
groupFunc={data => getObjectTypeFieldLabel(data.objectTypeField, driver)}

View File

@@ -6,7 +6,6 @@
import PluginsWidget from './PluginsWidget.svelte';
import CellDataWidget from './CellDataWidget.svelte';
import HistoryWidget from './HistoryWidget.svelte';
import AppWidget from './AppWidget.svelte';
import AdminMenuWidget from './AdminMenuWidget.svelte';
import AdminPremiumPromoWidget from './AdminPremiumPromoWidget.svelte';
import PublicCloudWidget from './PublicCloudWidget.svelte';
@@ -14,8 +13,9 @@
import hasPermission from '../utility/hasPermission';
</script>
<DatabaseWidget hidden={$visibleSelectedWidget != 'database'} />
{#if hasPermission('widgets/database')}
<DatabaseWidget hidden={$visibleSelectedWidget != 'database'} />
{/if}
{#if $visibleSelectedWidget == 'file' && hasPermission('widgets/file')}
<FilesWidget />
{/if}
@@ -31,9 +31,6 @@
{#if $visibleSelectedWidget == 'cell-data' && hasPermission('widgets/cell-data')}
<CellDataWidget />
{/if}
{#if $visibleSelectedWidget == 'app' && hasPermission('widgets/app')}
<AppWidget />
{/if}
{#if $visibleSelectedWidget == 'admin' && hasPermission('widgets/admin')}
<AdminMenuWidget />
{/if}

View File

@@ -110,13 +110,6 @@
hasPermission('settings/change') && { command: 'settings.show' },
{ command: 'theme.changeTheme' },
hasPermission('settings/change') && { command: 'settings.commands' },
hasPermission('widgets/app') && {
text: 'View applications',
onClick: () => {
$selectedWidget = 'app';
$visibleWidgetSideBar = true;
},
},
hasPermission('widgets/plugins') && {
text: 'Manage plugins',
onClick: () => {