mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-19 20:06:00 +00:00
Merge branch 'master' into feature/duckdb-2
This commit is contained in:
@@ -157,6 +157,7 @@
|
||||
}
|
||||
|
||||
.snackbar-container {
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: var(--dim-statusbar-height);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" context="module">
|
||||
function openArchive(fileName, folderName) {
|
||||
async function openArchive(fileName, folderName) {
|
||||
openNewTab({
|
||||
title: fileName,
|
||||
icon: 'img archive',
|
||||
@@ -10,17 +10,21 @@
|
||||
archiveFolder: folderName,
|
||||
},
|
||||
});
|
||||
// }
|
||||
}
|
||||
|
||||
async function openTextFile(fileName, fileType, folderName, tabComponent, icon) {
|
||||
const connProps: any = {};
|
||||
let tooltip = undefined;
|
||||
const isZipped = folderName.endsWith('.zip');
|
||||
|
||||
const resp = await apiCall('files/load', {
|
||||
folder: 'archive:' + folderName,
|
||||
file: fileName + '.' + fileType,
|
||||
format: 'text',
|
||||
});
|
||||
const resp = isZipped
|
||||
? await apiCall('files/download-text', { uri: `zip://archive:${folderName}//${fileName}.jsonl` })
|
||||
: await apiCall('files/load', {
|
||||
folder: 'archive:' + folderName,
|
||||
file: fileName + '.' + fileType,
|
||||
format: 'text',
|
||||
});
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
@@ -58,7 +62,7 @@
|
||||
if (data.fileType == 'jsonl') {
|
||||
return 'img archive';
|
||||
}
|
||||
return ARCHIVE_ICONS[data.fileType];
|
||||
return ARCHIVE_ICONS[data.fileType] ?? 'img anyfile';
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -79,6 +83,7 @@
|
||||
import { openImportExportTab } from '../utility/importExportTools';
|
||||
|
||||
export let data;
|
||||
$: isZipped = data.folderName?.endsWith('.zip');
|
||||
|
||||
const handleRename = () => {
|
||||
showModal(InputTextModal, {
|
||||
@@ -112,6 +117,9 @@
|
||||
openArchive(data.fileName, data.folderName);
|
||||
};
|
||||
const handleClick = () => {
|
||||
if (!data.fileType) {
|
||||
return;
|
||||
}
|
||||
if (data.fileType == 'jsonl') {
|
||||
handleOpenArchive();
|
||||
}
|
||||
@@ -133,11 +141,15 @@
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
if (!data.fileType) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
data.fileType == 'jsonl' && { text: 'Open', onClick: handleOpenArchive },
|
||||
data.fileType == 'jsonl' && { text: 'Open in text editor', onClick: handleOpenJsonLinesText },
|
||||
{ text: 'Delete', onClick: handleDelete },
|
||||
{ text: 'Rename', onClick: handleRename },
|
||||
!isZipped && { text: 'Delete', onClick: handleDelete },
|
||||
!isZipped && { text: 'Rename', onClick: handleRename },
|
||||
data.fileType == 'jsonl' &&
|
||||
createQuickExportMenu(
|
||||
fmt => async () => {
|
||||
@@ -174,29 +186,30 @@
|
||||
),
|
||||
data.fileType.endsWith('.sql') && { text: 'Open SQL', onClick: handleOpenSqlFile },
|
||||
data.fileType.endsWith('.yaml') && { text: 'Open YAML', onClick: handleOpenYamlFile },
|
||||
data.fileType == 'jsonl' && {
|
||||
text: 'Open in profiler',
|
||||
submenu: getExtensions()
|
||||
.drivers.filter(eng => eng.profilerFormatterFunction)
|
||||
.map(eng => ({
|
||||
text: eng.title,
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Profiler',
|
||||
icon: 'img profiler',
|
||||
tabComponent: 'ProfilerTab',
|
||||
props: {
|
||||
jslidLoad: `archive://${data.folderName}/${data.fileName}`,
|
||||
engine: eng.engine,
|
||||
// profilerFormatterFunction: eng.profilerFormatterFunction,
|
||||
// profilerTimestampFunction: eng.profilerTimestampFunction,
|
||||
// profilerChartAggregateFunction: eng.profilerChartAggregateFunction,
|
||||
// profilerChartMeasures: eng.profilerChartMeasures,
|
||||
},
|
||||
});
|
||||
},
|
||||
})),
|
||||
},
|
||||
!isZipped &&
|
||||
data.fileType == 'jsonl' && {
|
||||
text: 'Open in profiler',
|
||||
submenu: getExtensions()
|
||||
.drivers.filter(eng => eng.profilerFormatterFunction)
|
||||
.map(eng => ({
|
||||
text: eng.title,
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Profiler',
|
||||
icon: 'img profiler',
|
||||
tabComponent: 'ProfilerTab',
|
||||
props: {
|
||||
jslidLoad: `archive://${data.folderName}/${data.fileName}`,
|
||||
engine: eng.engine,
|
||||
// profilerFormatterFunction: eng.profilerFormatterFunction,
|
||||
// profilerTimestampFunction: eng.profilerTimestampFunction,
|
||||
// profilerChartAggregateFunction: eng.profilerChartAggregateFunction,
|
||||
// profilerChartMeasures: eng.profilerChartMeasures,
|
||||
},
|
||||
});
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { extractShellConnection } from '../impexp/createImpExpScript';
|
||||
import { saveFileToDisk } from '../utility/exportFileTools';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -100,7 +101,7 @@ await dbgateApi.deployDb(${JSON.stringify(
|
||||
props: {
|
||||
conid: $currentDatabase?.connection?._id,
|
||||
database: $currentDatabase?.name,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: {
|
||||
@@ -113,12 +114,12 @@ await dbgateApi.deployDb(${JSON.stringify(
|
||||
);
|
||||
};
|
||||
|
||||
const handleOpenDuplicatorTab = () => {
|
||||
const handleOpenDataDeployTab = () => {
|
||||
openNewTab(
|
||||
{
|
||||
title: data.name,
|
||||
icon: 'img duplicator',
|
||||
tabComponent: 'DataDuplicatorTab',
|
||||
icon: 'img data-deploy',
|
||||
tabComponent: 'DataDeployTab',
|
||||
props: {
|
||||
conid: $currentDatabase?.connection?._id,
|
||||
database: $currentDatabase?.name,
|
||||
@@ -127,21 +128,56 @@ await dbgateApi.deployDb(${JSON.stringify(
|
||||
{
|
||||
editor: {
|
||||
archiveFolder: data.name,
|
||||
conid: $currentDatabase?.connection?._id,
|
||||
database: $currentDatabase?.name,
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleZipUnzip = async method => {
|
||||
await apiCall(method, {
|
||||
folder: data.name,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadZip = async () => {
|
||||
saveFileToDisk(
|
||||
async filePath => {
|
||||
const zipped = await apiCall('archive/get-zipped-path', {
|
||||
folder: data.name,
|
||||
});
|
||||
await apiCall('files/simple-copy', {
|
||||
sourceFilePath: zipped.filePath,
|
||||
targetFilePath: filePath,
|
||||
});
|
||||
},
|
||||
{
|
||||
formatLabel: 'ZIP files',
|
||||
formatExtension: 'zip',
|
||||
defaultFileName: data.name?.endsWith('.zip') ? data.name : data.name + '.zip',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
data.name != 'default' && { text: 'Delete', onClick: handleDelete },
|
||||
data.name != 'default' && { text: 'Rename', onClick: handleRename },
|
||||
data.name != 'default' &&
|
||||
$currentDatabase && [
|
||||
{ text: 'Data duplicator', onClick: handleOpenDuplicatorTab },
|
||||
isProApp() && { text: 'Data deployer', onClick: handleOpenDataDeployTab },
|
||||
{ text: 'Generate deploy DB SQL', onClick: handleGenerateDeploySql },
|
||||
{ text: 'Shell: Deploy DB', onClick: handleGenerateDeployScript },
|
||||
],
|
||||
data.name != 'default' &&
|
||||
isProApp() &&
|
||||
data.name.endsWith('.zip') && { text: 'Unpack ZIP', onClick: () => handleZipUnzip('archive/unzip') },
|
||||
data.name != 'default' &&
|
||||
isProApp() &&
|
||||
!data.name.endsWith('.zip') && { text: 'Pack (create ZIP)', onClick: () => handleZipUnzip('archive/zip') },
|
||||
|
||||
isProApp() && { text: 'Download ZIP', onClick: handleDownloadZip },
|
||||
|
||||
data.name != 'default' &&
|
||||
hasPermission('dbops/model/compare') &&
|
||||
@@ -158,7 +194,7 @@ await dbgateApi.deployDb(${JSON.stringify(
|
||||
{...$$restProps}
|
||||
{data}
|
||||
title={data.name.endsWith('.link') ? data.name.slice(0, -5) : data.name}
|
||||
icon={data.name.endsWith('.link') ? 'img link' : 'img archive-folder'}
|
||||
icon={data.name.endsWith('.link') ? 'img link' : data.name.endsWith('.zip') ? 'img zipfile' : 'img archive-folder'}
|
||||
isBold={data.name == $currentArchive}
|
||||
on:click={() => ($currentArchive = data.name)}
|
||||
menu={createMenu}
|
||||
|
||||
@@ -330,15 +330,15 @@ await dbgateApi.dropAllDbObjects(${JSON.stringify(
|
||||
});
|
||||
};
|
||||
|
||||
const handleImportWithDbDuplicator = () => {
|
||||
const handleShowDataDeployer = () => {
|
||||
showModal(ChooseArchiveFolderModal, {
|
||||
message: 'Choose archive folder for import from',
|
||||
message: 'Choose archive folder for data deployer',
|
||||
onConfirm: archiveFolder => {
|
||||
openNewTab(
|
||||
{
|
||||
title: archiveFolder,
|
||||
icon: 'img duplicator',
|
||||
tabComponent: 'DataDuplicatorTab',
|
||||
icon: 'img replicator',
|
||||
tabComponent: 'DataDeployerTab',
|
||||
props: {
|
||||
conid: connection?._id,
|
||||
database: name,
|
||||
@@ -439,8 +439,8 @@ await dbgateApi.dropAllDbObjects(${JSON.stringify(
|
||||
|
||||
driver?.databaseEngineTypes?.includes('sql') &&
|
||||
hasPermission(`dbops/import`) && {
|
||||
onClick: handleImportWithDbDuplicator,
|
||||
text: 'Import with DB duplicator',
|
||||
onClick: handleShowDataDeployer,
|
||||
text: 'Data deployer',
|
||||
},
|
||||
|
||||
{ divider: true },
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
tabComponent: string;
|
||||
folder: string;
|
||||
currentConnection: boolean;
|
||||
extension: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const sql: FileTypeHandler = {
|
||||
@@ -15,6 +17,8 @@
|
||||
tabComponent: 'QueryTab',
|
||||
folder: 'sql',
|
||||
currentConnection: true,
|
||||
extension: 'sql',
|
||||
label: 'SQL file',
|
||||
};
|
||||
|
||||
const shell: FileTypeHandler = {
|
||||
@@ -23,6 +27,8 @@
|
||||
tabComponent: 'ShellTab',
|
||||
folder: 'shell',
|
||||
currentConnection: false,
|
||||
extension: 'js',
|
||||
label: 'JavaScript Shell script',
|
||||
};
|
||||
|
||||
const markdown: FileTypeHandler = {
|
||||
@@ -31,6 +37,8 @@
|
||||
tabComponent: 'MarkdownEditorTab',
|
||||
folder: 'markdown',
|
||||
currentConnection: false,
|
||||
extension: 'md',
|
||||
label: 'Markdown file',
|
||||
};
|
||||
|
||||
const charts: FileTypeHandler = {
|
||||
@@ -39,6 +47,8 @@
|
||||
tabComponent: 'ChartTab',
|
||||
folder: 'charts',
|
||||
currentConnection: true,
|
||||
extension: 'json',
|
||||
label: 'Chart file',
|
||||
};
|
||||
|
||||
const query: FileTypeHandler = {
|
||||
@@ -47,6 +57,8 @@
|
||||
tabComponent: 'QueryDesignTab',
|
||||
folder: 'query',
|
||||
currentConnection: true,
|
||||
extension: 'json',
|
||||
label: 'Query design file',
|
||||
};
|
||||
|
||||
const sqlite: FileTypeHandler = {
|
||||
@@ -55,6 +67,8 @@
|
||||
tabComponent: null,
|
||||
folder: 'sqlite',
|
||||
currentConnection: true,
|
||||
extension: 'sqlite',
|
||||
label: 'SQLite database',
|
||||
};
|
||||
|
||||
const diagrams: FileTypeHandler = {
|
||||
@@ -63,22 +77,52 @@
|
||||
tabComponent: 'DiagramTab',
|
||||
folder: 'diagrams',
|
||||
currentConnection: true,
|
||||
extension: 'json',
|
||||
label: 'Diagram file',
|
||||
};
|
||||
|
||||
const jobs: FileTypeHandler = {
|
||||
const impexp: FileTypeHandler = {
|
||||
icon: 'img export',
|
||||
format: 'json',
|
||||
tabComponent: 'ImportExportTab',
|
||||
folder: 'jobs',
|
||||
folder: 'impexp',
|
||||
currentConnection: false,
|
||||
extension: 'json',
|
||||
label: 'Import/Export file',
|
||||
};
|
||||
|
||||
const datadeploy: FileTypeHandler = isProApp()
|
||||
? {
|
||||
icon: 'img data-deploy',
|
||||
format: 'json',
|
||||
tabComponent: 'DataDeployTab',
|
||||
folder: 'datadeploy',
|
||||
currentConnection: false,
|
||||
extension: 'json',
|
||||
label: 'Data deploy file',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const dbcompare: FileTypeHandler = isProApp()
|
||||
? {
|
||||
icon: 'img compare',
|
||||
format: 'json',
|
||||
tabComponent: 'CompareModelTab',
|
||||
folder: 'dbcompare',
|
||||
currentConnection: false,
|
||||
extension: 'json',
|
||||
label: 'Database compare file',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const perspectives: FileTypeHandler = {
|
||||
icon: 'img perspective',
|
||||
format: 'json',
|
||||
tabComponent: 'PerspectiveTab',
|
||||
folder: 'pesrpectives',
|
||||
currentConnection: true,
|
||||
extension: 'json',
|
||||
label: 'Perspective file',
|
||||
};
|
||||
|
||||
const modtrans: FileTypeHandler = {
|
||||
@@ -87,6 +131,8 @@
|
||||
tabComponent: 'ModelTransformTab',
|
||||
folder: 'modtrans',
|
||||
currentConnection: false,
|
||||
extension: 'json',
|
||||
label: 'Model transform file',
|
||||
};
|
||||
|
||||
export const SAVED_FILE_HANDLERS = {
|
||||
@@ -98,8 +144,10 @@
|
||||
sqlite,
|
||||
diagrams,
|
||||
perspectives,
|
||||
jobs,
|
||||
impexp,
|
||||
modtrans,
|
||||
datadeploy,
|
||||
dbcompare,
|
||||
};
|
||||
|
||||
export const extractKey = data => data.file;
|
||||
@@ -122,6 +170,8 @@
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { saveFileToDisk } from '../utility/exportFileTools';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -148,6 +198,7 @@
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Create copy', onClick: handleCopy },
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Delete', onClick: handleDelete },
|
||||
folder == 'markdown' && { text: 'Show page', onClick: showMarkdownPage },
|
||||
{ text: 'Download', onClick: handleDownload },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -182,6 +233,19 @@
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
saveFileToDisk(
|
||||
async filePath => {
|
||||
await apiCall('files/export-file', {
|
||||
folder,
|
||||
file: data.file,
|
||||
filePath,
|
||||
});
|
||||
},
|
||||
{ formatLabel: handler.label, formatExtension: handler.format, defaultFileName: data.file }
|
||||
);
|
||||
};
|
||||
|
||||
async function openTab() {
|
||||
const resp = await apiCall('files/load', { folder, file: data.file, format: handler.format });
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
label:hover:not(.disabled) {
|
||||
|
||||
61
packages/web/src/buttons/InlineUploadButton.svelte
Normal file
61
packages/web/src/buttons/InlineUploadButton.svelte
Normal file
@@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import InlineButton from '../buttons/InlineButton.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import InlineButtonLabel from '../buttons/InlineButtonLabel.svelte';
|
||||
import resolveApi, { resolveApiHeaders } from '../utility/resolveApi';
|
||||
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
export let filters;
|
||||
export let onProcessFile;
|
||||
export let icon = 'icon plus-thick';
|
||||
|
||||
const inputId = `uploadFileButton-${uuidv1()}`;
|
||||
|
||||
const electron = getElectron();
|
||||
|
||||
async function handleUploadedFile(e) {
|
||||
const files = [...e.target.files];
|
||||
|
||||
for (const file of files) {
|
||||
const formData = new FormData();
|
||||
formData.append('name', file.name);
|
||||
formData.append('data', file);
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: resolveApiHeaders(),
|
||||
};
|
||||
|
||||
const apiBase = resolveApi();
|
||||
const resp = await fetch(`${apiBase}/uploads/upload`, fetchOptions);
|
||||
const { filePath, originalName } = await resp.json();
|
||||
await onProcessFile(filePath, originalName);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOpenElectronFile() {
|
||||
const filePaths = await electron.showOpenDialog({
|
||||
filters,
|
||||
properties: ['showHiddenFiles', 'openFile'],
|
||||
});
|
||||
const filePath = filePaths && filePaths[0];
|
||||
if (!filePath) return;
|
||||
onProcessFile(filePath, filePath.split(/[\/\\]/).pop());
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if electron}
|
||||
<InlineButton on:click={handleOpenElectronFile} title="Open file" data-testid={$$props['data-testid']}>
|
||||
<FontIcon {icon} />
|
||||
</InlineButton>
|
||||
{:else}
|
||||
<InlineButtonLabel on:click={() => {}} title="Upload file" data-testid={$$props['data-testid']} htmlFor={inputId}>
|
||||
<FontIcon {icon} />
|
||||
</InlineButtonLabel>
|
||||
{/if}
|
||||
|
||||
<input type="file" id={inputId} hidden on:change={handleUploadedFile} />
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="button" on:click={handleClick} class:disabled class:fillHorizontal>
|
||||
<div class="button" on:click={handleClick} class:disabled class:fillHorizontal data-testid={$$props['data-testid']}>
|
||||
<div class="icon">
|
||||
<FontIcon {icon} />
|
||||
</div>
|
||||
|
||||
@@ -47,6 +47,7 @@ import newTable from '../tableeditor/newTable';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { openWebLink } from '../utility/simpleTools';
|
||||
import { _t } from '../translations';
|
||||
import ExportImportConnectionsModal from '../modals/ExportImportConnectionsModal.svelte';
|
||||
|
||||
// function themeCommand(theme: ThemeDefinition) {
|
||||
// return {
|
||||
@@ -530,6 +531,44 @@ registerCommand({
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'app.exportConnections',
|
||||
category: 'Settings',
|
||||
name: 'Export connections',
|
||||
testEnabled: () => getElectron() != null,
|
||||
onClick: () => {
|
||||
showModal(ExportImportConnectionsModal, {
|
||||
mode: 'export',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'app.importConnections',
|
||||
category: 'Settings',
|
||||
name: 'Import connections',
|
||||
testEnabled: () => getElectron() != null,
|
||||
onClick: async () => {
|
||||
const files = await electron.showOpenDialog({
|
||||
properties: ['showHiddenFiles', 'openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: `All supported files`,
|
||||
extensions: ['zip'],
|
||||
},
|
||||
{ name: `ZIP files`, extensions: ['zip'] },
|
||||
],
|
||||
});
|
||||
|
||||
if (files?.length > 0) {
|
||||
showModal(ExportImportConnectionsModal, {
|
||||
mode: 'import',
|
||||
uploadedFilePath: files[0],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'file.import',
|
||||
category: 'File',
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
export let value;
|
||||
export let jsonParsedValue = undefined;
|
||||
export let editorTypes;
|
||||
export let rightMargin = false;
|
||||
|
||||
$: stringified = stringifyCellValue(
|
||||
value,
|
||||
@@ -20,7 +21,7 @@
|
||||
{#if rowData == null}
|
||||
<span class="null">(No row)</span>
|
||||
{:else}
|
||||
<span class={stringified.gridStyle} title={stringified.gridTitle}>{stringified.value}</span>
|
||||
<span class={stringified.gridStyle} title={stringified.gridTitle} class:rightMargin>{stringified.value}</span>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
@@ -31,4 +32,8 @@
|
||||
.valueCellStyle {
|
||||
color: var(--theme-icon-green);
|
||||
}
|
||||
|
||||
.rightMargin {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
export let isModifiedCell = false;
|
||||
export let isInserted = false;
|
||||
export let isDeleted = false;
|
||||
export let isMissing = false;
|
||||
export let isAutofillSelected = false;
|
||||
export let isFocusedColumn = false;
|
||||
export let domCell = undefined;
|
||||
@@ -33,6 +34,9 @@
|
||||
export let onSetValue;
|
||||
export let editorTypes = null;
|
||||
export let isReadonly;
|
||||
export let hasOverlayValue = false;
|
||||
export let overlayValue = null;
|
||||
export let isMissingOverlayField = false;
|
||||
|
||||
$: value = col.isStructured ? _.get(rowData || {}, col.uniquePath) : (rowData || {})[col.uniqueName];
|
||||
|
||||
@@ -68,69 +72,88 @@
|
||||
class:isModifiedCell
|
||||
class:isInserted
|
||||
class:isDeleted
|
||||
class:isMissing
|
||||
class:isAutofillSelected
|
||||
class:isFocusedColumn
|
||||
class:hasOverlayValue
|
||||
class:isMissingOverlayField
|
||||
class:alignRight={_.isNumber(value) && !showHint}
|
||||
{style}
|
||||
>
|
||||
<CellValue {rowData} {value} {jsonParsedValue} {editorTypes} />
|
||||
|
||||
{#if showHint}
|
||||
<span class="hint"
|
||||
>{col.hintColumnNames.map(hintColumnName => rowData[hintColumnName]).join(col.hintColumnDelimiter || ' ')}</span
|
||||
>
|
||||
{/if}
|
||||
|
||||
{#if editorTypes?.explicitDataType}
|
||||
{#if value !== undefined}
|
||||
<ShowFormDropDownButton
|
||||
icon={detectTypeIcon(value)}
|
||||
menu={() => getConvertValueMenu(value, onSetValue, editorTypes)}
|
||||
/>
|
||||
{#if hasOverlayValue}
|
||||
<div class="flex1 flex">
|
||||
<div class="replacedValue overlayCell overlayCell1">
|
||||
<CellValue {rowData} {value} {jsonParsedValue} {editorTypes} />
|
||||
</div>
|
||||
<div class="overlayCell overlayCell2">
|
||||
<CellValue {rowData} value={overlayValue} {editorTypes} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<CellValue
|
||||
{rowData}
|
||||
{value}
|
||||
{jsonParsedValue}
|
||||
{editorTypes}
|
||||
rightMargin={_.isNumber(value) && !showHint && (editorTypes?.explicitDataType || col.foreignKey)}
|
||||
/>
|
||||
{#if showHint}
|
||||
<span class="hint"
|
||||
>{col.hintColumnNames.map(hintColumnName => rowData[hintColumnName]).join(col.hintColumnDelimiter || ' ')}</span
|
||||
>
|
||||
{/if}
|
||||
{#if _.isPlainObject(value)}
|
||||
<ShowFormButton secondary icon="icon open-in-new" on:click={() => openJsonDocument(value, undefined, true)} />
|
||||
{/if}
|
||||
{#if _.isArray(value)}
|
||||
|
||||
{#if editorTypes?.explicitDataType}
|
||||
{#if value !== undefined}
|
||||
<ShowFormDropDownButton
|
||||
icon={detectTypeIcon(value)}
|
||||
menu={() => getConvertValueMenu(value, onSetValue, editorTypes)}
|
||||
/>
|
||||
{/if}
|
||||
{#if _.isPlainObject(value)}
|
||||
<ShowFormButton secondary icon="icon open-in-new" on:click={() => openJsonDocument(value, undefined, true)} />
|
||||
{/if}
|
||||
{#if _.isArray(value)}
|
||||
<ShowFormButton
|
||||
secondary
|
||||
icon="icon open-in-new"
|
||||
on:click={() => {
|
||||
if (_.every(value, x => _.isPlainObject(x))) {
|
||||
openJsonLinesData(value);
|
||||
} else {
|
||||
openJsonDocument(value, undefined, true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{:else if col.foreignKey && rowData && rowData[col.uniqueName] && !isCurrentCell}
|
||||
<ShowFormButton on:click={() => onSetFormView(rowData, col)} />
|
||||
{:else if col.foreignKey && isCurrentCell && onDictionaryLookup && !isReadonly}
|
||||
<ShowFormButton icon="icon dots-horizontal" on:click={onDictionaryLookup} />
|
||||
{:else if isJson}
|
||||
<ShowFormButton icon="icon open-in-new" on:click={() => openJsonDocument(value, undefined, true)} />
|
||||
{:else if jsonParsedValue && _.isPlainObject(jsonParsedValue)}
|
||||
<ShowFormButton icon="icon open-in-new" on:click={() => openJsonDocument(jsonParsedValue, undefined, true)} />
|
||||
{:else if _.isArray(jsonParsedValue || value)}
|
||||
<ShowFormButton
|
||||
secondary
|
||||
icon="icon open-in-new"
|
||||
on:click={() => {
|
||||
if (_.every(value, x => _.isPlainObject(x))) {
|
||||
openJsonLinesData(value);
|
||||
if (_.every(jsonParsedValue || value, x => _.isPlainObject(x))) {
|
||||
openJsonLinesData(jsonParsedValue || value);
|
||||
} else {
|
||||
openJsonDocument(value, undefined, true);
|
||||
openJsonDocument(jsonParsedValue || value, undefined, true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{:else if col.foreignKey && rowData && rowData[col.uniqueName] && !isCurrentCell}
|
||||
<ShowFormButton on:click={() => onSetFormView(rowData, col)} />
|
||||
{:else if col.foreignKey && isCurrentCell && onDictionaryLookup && !isReadonly}
|
||||
<ShowFormButton icon="icon dots-horizontal" on:click={onDictionaryLookup} />
|
||||
{:else if isJson}
|
||||
<ShowFormButton icon="icon open-in-new" on:click={() => openJsonDocument(value, undefined, true)} />
|
||||
{:else if jsonParsedValue && _.isPlainObject(jsonParsedValue)}
|
||||
<ShowFormButton icon="icon open-in-new" on:click={() => openJsonDocument(jsonParsedValue, undefined, true)} />
|
||||
{:else if _.isArray(jsonParsedValue || value)}
|
||||
<ShowFormButton
|
||||
icon="icon open-in-new"
|
||||
on:click={() => {
|
||||
if (_.every(jsonParsedValue || value, x => _.isPlainObject(x))) {
|
||||
openJsonLinesData(jsonParsedValue || value);
|
||||
} else {
|
||||
openJsonDocument(jsonParsedValue || value, undefined, true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isAutoFillMarker}
|
||||
<div class="autoFillMarker autofillHandleMarker" />
|
||||
{/if}
|
||||
{#if isAutoFillMarker}
|
||||
<div class="autoFillMarker autofillHandleMarker" />
|
||||
{/if}
|
||||
|
||||
{#if showSlot}
|
||||
<slot />
|
||||
{#if showSlot}
|
||||
<slot />
|
||||
{/if}
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
@@ -175,6 +198,9 @@
|
||||
td.isDeleted {
|
||||
background: var(--theme-bg-volcano);
|
||||
}
|
||||
td.isMissing {
|
||||
background: var(--theme-bg-volcano);
|
||||
}
|
||||
td.isSelected {
|
||||
background: var(--theme-bg-3);
|
||||
}
|
||||
@@ -182,9 +208,9 @@
|
||||
background: var(--theme-bg-selected);
|
||||
}
|
||||
td.isDeleted {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
|
||||
background-repeat: repeat-x;
|
||||
background-position: 50% 50%;
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==') !important;
|
||||
background-repeat: repeat-x !important;
|
||||
background-position: 50% 50% !important;
|
||||
}
|
||||
|
||||
.hint {
|
||||
@@ -207,4 +233,31 @@
|
||||
color: var(--theme-icon-green);
|
||||
text-align: var(--data-grid-numbers-align);
|
||||
}
|
||||
|
||||
.hasOverlayValue .overlayCell {
|
||||
width: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hasOverlayValue .overlayCell1 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.hasOverlayValue .overlayCell2 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.replacedValue {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
|
||||
background-repeat: repeat-x;
|
||||
background-position: 50% 50%;
|
||||
}
|
||||
|
||||
td.isMissingOverlayField {
|
||||
background: var(--theme-bg-orange);
|
||||
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
|
||||
background-repeat: repeat-x;
|
||||
background-position: 50% 50%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -282,48 +282,59 @@
|
||||
testEnabled: () => getCurrentDataGrid()?.editCellValueEnabled(),
|
||||
onClick: () => getCurrentDataGrid().editCellValue(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.mergeSelectedCellsIntoMirror',
|
||||
category: 'Data grid',
|
||||
name: 'Merge selected cells',
|
||||
testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'merge', fullRows: false }),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.mergeSelectedRowsIntoMirror',
|
||||
category: 'Data grid',
|
||||
name: 'Merge selected rows',
|
||||
testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'merge', fullRows: true }),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.appendSelectedCellsIntoMirror',
|
||||
category: 'Data grid',
|
||||
name: 'Append selected cells',
|
||||
testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'append', fullRows: false }),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.appendSelectedRowsIntoMirror',
|
||||
category: 'Data grid',
|
||||
name: 'Append selected rows',
|
||||
testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'append', fullRows: true }),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.replaceSelectedCellsIntoMirror',
|
||||
category: 'Data grid',
|
||||
name: 'Replace with selected cells',
|
||||
testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'replace', fullRows: false }),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.replaceSelectedRowsIntoMirror',
|
||||
category: 'Data grid',
|
||||
name: 'Replace with selected rows',
|
||||
testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'replace', fullRows: true }),
|
||||
});
|
||||
|
||||
if (isProApp()) {
|
||||
registerCommand({
|
||||
id: 'dataGrid.sendToDataDeploy',
|
||||
category: 'Data grid',
|
||||
name: 'Send to data deployer',
|
||||
testEnabled: () => getCurrentDataGrid()?.sendToDataDeployEnabled(),
|
||||
onClick: () => getCurrentDataGrid().sendToDataDeploy(),
|
||||
});
|
||||
}
|
||||
|
||||
// registerCommand({
|
||||
// id: 'dataGrid.mergeSelectedCellsIntoMirror',
|
||||
// category: 'Data grid',
|
||||
// name: 'Merge selected cells',
|
||||
// testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
// onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'merge', fullRows: false }),
|
||||
// });
|
||||
// registerCommand({
|
||||
// id: 'dataGrid.mergeSelectedRowsIntoMirror',
|
||||
// category: 'Data grid',
|
||||
// name: 'Merge selected rows',
|
||||
// testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
// onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'merge', fullRows: true }),
|
||||
// });
|
||||
// registerCommand({
|
||||
// id: 'dataGrid.appendSelectedCellsIntoMirror',
|
||||
// category: 'Data grid',
|
||||
// name: 'Append selected cells',
|
||||
// testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
// onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'append', fullRows: false }),
|
||||
// });
|
||||
// registerCommand({
|
||||
// id: 'dataGrid.appendSelectedRowsIntoMirror',
|
||||
// category: 'Data grid',
|
||||
// name: 'Append selected rows',
|
||||
// testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
// onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'append', fullRows: true }),
|
||||
// });
|
||||
// registerCommand({
|
||||
// id: 'dataGrid.replaceSelectedCellsIntoMirror',
|
||||
// category: 'Data grid',
|
||||
// name: 'Replace with selected cells',
|
||||
// testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
// onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'replace', fullRows: false }),
|
||||
// });
|
||||
// registerCommand({
|
||||
// id: 'dataGrid.replaceSelectedRowsIntoMirror',
|
||||
// category: 'Data grid',
|
||||
// name: 'Replace with selected rows',
|
||||
// testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
// onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'replace', fullRows: true }),
|
||||
// });
|
||||
|
||||
function getSelectedCellsInfo(selectedCells, grider, realColumnUniqueNames, selectedRowData) {
|
||||
if (selectedCells.length > 1 && selectedCells.every(x => _.isNumber(x[0]) && _.isNumber(x[1]))) {
|
||||
@@ -418,6 +429,8 @@
|
||||
import contextMenuActivator from '../utility/contextMenuActivator';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { _t } from '../translations';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import SaveArchiveModal from '../modals/SaveArchiveModal.svelte';
|
||||
|
||||
export let onLoadNextData = undefined;
|
||||
export let grider = undefined;
|
||||
@@ -454,6 +467,8 @@
|
||||
export let jslid;
|
||||
// export let generalAllowSave = false;
|
||||
export let hideGridLeftColumn = false;
|
||||
export let overlayDefinition = null;
|
||||
export let onGetSelectionMenu = null;
|
||||
|
||||
export const activator = createActivator('DataGridCore', false);
|
||||
|
||||
@@ -482,6 +497,7 @@
|
||||
const domFilterControlsRef = createRef({});
|
||||
|
||||
let isGridFocused = false;
|
||||
let selectionMenu = null;
|
||||
|
||||
const tabid = getContext('tabid');
|
||||
|
||||
@@ -1003,11 +1019,11 @@
|
||||
});
|
||||
}
|
||||
|
||||
export function mirrorWriteEnabled(requireKey) {
|
||||
return requireKey ? !!display.baseTable?.primaryKey || !!display.baseCollection : !!display.baseTableOrSimilar;
|
||||
export function sendToDataDeployEnabled() {
|
||||
return !!display.baseTable?.primaryKey || !!display.baseCollection;
|
||||
}
|
||||
|
||||
export async function mergeSelectionIntoMirror({ fullRows, mergeMode = 'merge' }) {
|
||||
export async function sendToDataDeploy() {
|
||||
const file = display.baseTableOrSimilar?.pureName;
|
||||
const mergeKey = display.baseCollection
|
||||
? display.baseCollection?.uniqueKey?.map(x => x.columnName)
|
||||
@@ -1019,20 +1035,77 @@
|
||||
const rows = rowIndexes.map(rowIndex => grider.getRowData(rowIndex));
|
||||
// @ts-ignore
|
||||
const columns = colIndexes.map(col => realColumnUniqueNames[col]);
|
||||
const mergedRows = fullRows ? rows : rows.map(x => _.pick(x, _.uniq([...columns, ...mergeKey])));
|
||||
|
||||
const res = await apiCall('archive/modify-file', {
|
||||
const mergedRows = rows.map(x => _.pick(x, _.uniq([...columns, ...mergeKey])));
|
||||
|
||||
showModal(SaveArchiveModal, {
|
||||
folder: $currentArchive,
|
||||
file,
|
||||
mergedRows,
|
||||
mergeKey,
|
||||
mergeMode,
|
||||
fileIsReadOnly: true,
|
||||
onSave: async folder => {
|
||||
const res = await apiCall('archive/modify-file', {
|
||||
folder,
|
||||
file,
|
||||
mergedRows,
|
||||
mergeKey,
|
||||
mergeMode: 'merge',
|
||||
});
|
||||
if (res) {
|
||||
showSnackbarSuccess(`Merged ${mergedRows.length} rows into ${file} in archive ${folder}`);
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: folder,
|
||||
icon: 'img data-deploy',
|
||||
tabComponent: 'DataDeployTab',
|
||||
props: {
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: {
|
||||
archiveFolder: folder,
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (res) {
|
||||
showSnackbarSuccess(`Merged ${mergedRows.length} rows into ${file} in archive ${$currentArchive}`);
|
||||
}
|
||||
}
|
||||
|
||||
// export function mirrorWriteEnabled(requireKey) {
|
||||
// return requireKey ? !!display.baseTable?.primaryKey || !!display.baseCollection : !!display.baseTableOrSimilar;
|
||||
// }
|
||||
|
||||
// export async function mergeSelectionIntoMirror({ fullRows, mergeMode = 'merge' }) {
|
||||
// const file = display.baseTableOrSimilar?.pureName;
|
||||
// const mergeKey = display.baseCollection
|
||||
// ? display.baseCollection?.uniqueKey?.map(x => x.columnName)
|
||||
// : display.baseTable?.primaryKey.columns.map(x => x.columnName);
|
||||
|
||||
// const cells = cellsToRegularCells(selectedCells);
|
||||
// const rowIndexes = _.sortBy(_.uniq(cells.map(x => x[0])));
|
||||
// const colIndexes = _.sortBy(_.uniq(cells.map(x => x[1])));
|
||||
// const rows = rowIndexes.map(rowIndex => grider.getRowData(rowIndex));
|
||||
// // @ts-ignore
|
||||
// const columns = colIndexes.map(col => realColumnUniqueNames[col]);
|
||||
// const mergedRows = fullRows ? rows : rows.map(x => _.pick(x, _.uniq([...columns, ...mergeKey])));
|
||||
|
||||
// const res = await apiCall('archive/modify-file', {
|
||||
// folder: $currentArchive,
|
||||
// file,
|
||||
// mergedRows,
|
||||
// mergeKey,
|
||||
// mergeMode,
|
||||
// });
|
||||
// if (res) {
|
||||
// showSnackbarSuccess(`Merged ${mergedRows.length} rows into ${file} in archive ${$currentArchive}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
export function canShowLeftPanel() {
|
||||
return !hideGridLeftColumn;
|
||||
}
|
||||
@@ -1152,8 +1225,16 @@
|
||||
onChangeSelectedColumns(getSelectedColumns().map(x => x.columnName));
|
||||
}
|
||||
|
||||
let publishedCells = null;
|
||||
|
||||
if (onPublishedCellsChanged) {
|
||||
onPublishedCellsChanged(getCellsPublished(selectedCells));
|
||||
if (!publishedCells) publishedCells = getCellsPublished(selectedCells);
|
||||
onPublishedCellsChanged(publishedCells);
|
||||
}
|
||||
|
||||
if (onGetSelectionMenu) {
|
||||
if (!publishedCells) publishedCells = getCellsPublished(selectedCells);
|
||||
selectionMenu = onGetSelectionMenu(publishedCells);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1192,6 +1273,7 @@
|
||||
engine: display?.driver,
|
||||
condition: display?.getChangeSetCondition(rowData),
|
||||
insertedRowIndex: grider?.getInsertedRowIndex(row),
|
||||
rowStatus: grider.getRowStatus(row),
|
||||
};
|
||||
})
|
||||
.filter(x => x.column);
|
||||
@@ -1747,14 +1829,14 @@
|
||||
{ placeTag: 'save' },
|
||||
{ command: 'dataGrid.revertRowChanges', hideDisabled: true },
|
||||
{ command: 'dataGrid.revertAllChanges', hideDisabled: true },
|
||||
{ command: 'dataGrid.deleteSelectedRows' },
|
||||
{ command: 'dataGrid.insertNewRow' },
|
||||
{ command: 'dataGrid.cloneRows' },
|
||||
{ command: 'dataGrid.deleteSelectedRows', hideDisabled: true },
|
||||
{ command: 'dataGrid.insertNewRow', hideDisabled: true },
|
||||
{ command: 'dataGrid.cloneRows', hideDisabled: true },
|
||||
{ command: 'dataGrid.setNull', hideDisabled: true },
|
||||
{ command: 'dataGrid.removeField', hideDisabled: true },
|
||||
{ placeTag: 'edit' },
|
||||
{ divider: true },
|
||||
{ command: 'dataGrid.findColumn' },
|
||||
{ command: 'dataGrid.findColumn', hideDisabled: true },
|
||||
{ command: 'dataGrid.hideColumn', hideDisabled: true },
|
||||
{ command: 'dataGrid.filterSelected' },
|
||||
{ command: 'dataGrid.clearFilter' },
|
||||
@@ -1773,17 +1855,18 @@
|
||||
// { command: 'dataGrid.copyJsonDocument', hideDisabled: true },
|
||||
{ divider: true },
|
||||
{ placeTag: 'export' },
|
||||
{
|
||||
label: 'Save to current archive',
|
||||
submenu: [
|
||||
{ command: 'dataGrid.mergeSelectedCellsIntoMirror' },
|
||||
{ command: 'dataGrid.mergeSelectedRowsIntoMirror' },
|
||||
{ command: 'dataGrid.appendSelectedCellsIntoMirror' },
|
||||
{ command: 'dataGrid.appendSelectedRowsIntoMirror' },
|
||||
{ command: 'dataGrid.replaceSelectedCellsIntoMirror' },
|
||||
{ command: 'dataGrid.replaceSelectedRowsIntoMirror' },
|
||||
],
|
||||
},
|
||||
// {
|
||||
// label: 'Save to current archive',
|
||||
// submenu: [
|
||||
// { command: 'dataGrid.mergeSelectedCellsIntoMirror' },
|
||||
// { command: 'dataGrid.mergeSelectedRowsIntoMirror' },
|
||||
// { command: 'dataGrid.appendSelectedCellsIntoMirror' },
|
||||
// { command: 'dataGrid.appendSelectedRowsIntoMirror' },
|
||||
// { command: 'dataGrid.replaceSelectedCellsIntoMirror' },
|
||||
// { command: 'dataGrid.replaceSelectedRowsIntoMirror' },
|
||||
// ],
|
||||
// },
|
||||
isProApp() && { command: 'dataGrid.sendToDataDeploy' },
|
||||
{ command: 'dataGrid.generateSqlFromData' },
|
||||
{ command: 'dataGrid.openFreeTable' },
|
||||
{ command: 'dataGrid.openChartFromSelection' },
|
||||
@@ -2017,6 +2100,7 @@
|
||||
onSetFormView={formViewAvailable && display?.baseTable?.primaryKey ? handleSetFormView : null}
|
||||
{dataEditorTypesBehaviourOverride}
|
||||
{gridColoringMode}
|
||||
{overlayDefinition}
|
||||
/>
|
||||
{/each}
|
||||
</tbody>
|
||||
@@ -2053,7 +2137,19 @@
|
||||
on:scroll={e => (firstVisibleRowScrollIndex = e.detail)}
|
||||
bind:this={domVerticalScroll}
|
||||
/>
|
||||
{#if selectedCellsInfo}
|
||||
{#if selectionMenu}
|
||||
<div class="selection-menu">
|
||||
{#each selectionMenu as item}
|
||||
<InlineButton
|
||||
on:click={() => {
|
||||
item.onClick();
|
||||
}}
|
||||
>
|
||||
{item.text}
|
||||
</InlineButton>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if selectedCellsInfo}
|
||||
<div class="row-count-label">
|
||||
{selectedCellsInfo}
|
||||
</div>
|
||||
@@ -2118,6 +2214,13 @@
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
.selection-menu {
|
||||
position: absolute;
|
||||
background-color: var(--theme-bg-2);
|
||||
right: 40px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
.no-rows-info {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
<script lang="ts" context="module">
|
||||
const OVERLAY_STATUS_ICONS = {
|
||||
regular: 'icon equal',
|
||||
updated: 'icon not-equal',
|
||||
missing: 'img table',
|
||||
inserted: 'img archive',
|
||||
};
|
||||
const OVERLAY_STATUS_TOOLTIPS = {
|
||||
regular: 'Row is the same in database and archive',
|
||||
updated: 'Row is different in database and archive',
|
||||
missing: 'Row is only in database',
|
||||
inserted: 'Row is only in archive',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import openReferenceForm from '../formview/openReferenceForm';
|
||||
import DictionaryLookupModal from '../modals/DictionaryLookupModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
|
||||
@@ -27,6 +41,7 @@
|
||||
export let database;
|
||||
export let driver;
|
||||
export let gridColoringMode = '36';
|
||||
export let overlayDefinition = null;
|
||||
|
||||
export let dataEditorTypesBehaviourOverride = null;
|
||||
|
||||
@@ -51,10 +66,17 @@
|
||||
onConfirm: value => grider.setCellValue(rowIndex, col.uniqueName, value),
|
||||
});
|
||||
}
|
||||
|
||||
// $: console.log('rowStatus', rowStatus);
|
||||
</script>
|
||||
|
||||
<tr style={`height: ${rowHeight}px`} class={`coloring-mode-${gridColoringMode}`}>
|
||||
<RowHeaderCell {rowIndex} onShowForm={onSetFormView ? () => onSetFormView(rowData, null) : null} />
|
||||
<RowHeaderCell
|
||||
{rowIndex}
|
||||
onShowForm={onSetFormView && !overlayDefinition ? () => onSetFormView(rowData, null) : null}
|
||||
extraIcon={overlayDefinition ? OVERLAY_STATUS_ICONS[rowStatus.status] : null}
|
||||
extraIconTooltip={overlayDefinition ? OVERLAY_STATUS_TOOLTIPS[rowStatus.status] : null}
|
||||
/>
|
||||
{#each visibleRealColumns as col (col.uniqueName)}
|
||||
{#if inplaceEditorState.cell && rowIndex == inplaceEditorState.cell[0] && col.colIndex == inplaceEditorState.cell[1]}
|
||||
<InplaceEditor
|
||||
@@ -83,11 +105,15 @@
|
||||
isAutofillSelected={cellIsSelected(rowIndex, col.colIndex, autofillSelectedCells)}
|
||||
isFocusedColumn={focusedColumns?.includes(col.uniqueName)}
|
||||
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
|
||||
overlayValue={rowStatus.overlayFields?.[col.uniqueName]}
|
||||
hasOverlayValue={rowStatus.overlayFields && col.uniqueName in rowStatus.overlayFields}
|
||||
isMissingOverlayField={rowStatus.missingOverlayFields && rowStatus.missingOverlayFields.has(col.uniqueName)}
|
||||
isModifiedRow={rowStatus.status == 'updated'}
|
||||
isInserted={rowStatus.status == 'inserted' ||
|
||||
(rowStatus.insertedFields && rowStatus.insertedFields.has(col.uniqueName))}
|
||||
isDeleted={rowStatus.status == 'deleted' ||
|
||||
(rowStatus.deletedFields && rowStatus.deletedFields.has(col.uniqueName))}
|
||||
isMissing={rowStatus.status == 'missing'}
|
||||
{onSetFormView}
|
||||
{isDynamicStructure}
|
||||
isAutoFillMarker={autofillMarkerCell &&
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export interface GriderRowStatus {
|
||||
status: 'regular' | 'updated' | 'deleted' | 'inserted';
|
||||
status: 'regular' | 'updated' | 'deleted' | 'inserted' | 'missing';
|
||||
modifiedFields?: Set<string>;
|
||||
insertedFields?: Set<string>;
|
||||
deletedFields?: Set<string>;
|
||||
overlayFields?: { [field: string]: string };
|
||||
missingOverlayFields?: Set<string>;
|
||||
}
|
||||
|
||||
export default abstract class Grider {
|
||||
@@ -61,4 +63,7 @@ export default abstract class Grider {
|
||||
this.setCellValue(index, key, changeObject[key]);
|
||||
}
|
||||
}
|
||||
getInsertedRowIndex(index) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
export let allowChangeChangeSetStructure = false;
|
||||
export let infoLoadCounter = 0;
|
||||
|
||||
export let driver;
|
||||
export let driver = null;
|
||||
|
||||
let loadedRows;
|
||||
let infoCounter = 0;
|
||||
|
||||
110
packages/web/src/datagrid/OverlayDiffGrider.ts
Normal file
110
packages/web/src/datagrid/OverlayDiffGrider.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { GridDisplay } from 'dbgate-datalib';
|
||||
import Grider from './Grider';
|
||||
import { GriderRowStatus } from './Grider';
|
||||
import _uniq from 'lodash/uniq';
|
||||
|
||||
export default class OverlayDiffGrider extends Grider {
|
||||
private prependRows: any[];
|
||||
private rowCacheIndexes: Set<number>;
|
||||
private rowDataCache;
|
||||
private rowStatusCache;
|
||||
private overlayRowsByStr: { [key: string]: any };
|
||||
|
||||
constructor(
|
||||
public sourceRows: any[],
|
||||
public display: GridDisplay,
|
||||
public matchColumns: string[],
|
||||
public overlayData: any[],
|
||||
public matchedDbKeys: any[][]
|
||||
) {
|
||||
super();
|
||||
const matchedDbKeysByStr = new Set(matchedDbKeys.map(x => x.join('||')));
|
||||
this.prependRows = overlayData.filter(row => !matchedDbKeysByStr.has(matchColumns.map(x => row[x]).join('||')));
|
||||
this.overlayRowsByStr = {};
|
||||
for (const row of overlayData) {
|
||||
const key = matchColumns.map(x => row[x]).join('||');
|
||||
this.overlayRowsByStr[key] = row;
|
||||
}
|
||||
|
||||
this.rowDataCache = {};
|
||||
this.rowStatusCache = {};
|
||||
this.rowCacheIndexes = new Set();
|
||||
}
|
||||
|
||||
requireRowCache(index: number) {
|
||||
if (this.rowCacheIndexes.has(index)) return;
|
||||
|
||||
if (index < this.prependRows.length) {
|
||||
this.rowStatusCache[index] = {
|
||||
status: 'inserted',
|
||||
};
|
||||
this.rowDataCache[index] = this.prependRows[index];
|
||||
this.rowCacheIndexes.add(index);
|
||||
return;
|
||||
}
|
||||
|
||||
const row = this.sourceRows[index - this.prependRows.length];
|
||||
|
||||
if (!row) {
|
||||
this.rowStatusCache[index] = {
|
||||
status: 'missing',
|
||||
};
|
||||
this.rowDataCache[index] = row;
|
||||
this.rowCacheIndexes.add(index);
|
||||
return;
|
||||
}
|
||||
|
||||
const overlayKey = this.matchColumns.map(x => row[x]).join('||');
|
||||
const overlayRow = this.overlayRowsByStr[overlayKey];
|
||||
|
||||
if (!overlayRow) {
|
||||
this.rowStatusCache[index] = {
|
||||
status: 'missing',
|
||||
};
|
||||
this.rowDataCache[index] = row;
|
||||
this.rowCacheIndexes.add(index);
|
||||
return;
|
||||
}
|
||||
|
||||
const overlayFields = {};
|
||||
const missingOverlayFields = new Set();
|
||||
|
||||
for (const field of this.display.columns.map(x => x.columnName)) {
|
||||
if (!(field in overlayRow)) {
|
||||
missingOverlayFields.add(field);
|
||||
} else if (row[field] != overlayRow[field]) {
|
||||
overlayFields[field] = overlayRow[field];
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(overlayFields).length > 0 || missingOverlayFields.size > 0) {
|
||||
this.rowStatusCache[index] = {
|
||||
status: 'updated',
|
||||
overlayFields,
|
||||
missingOverlayFields,
|
||||
modifiedFields: new Set(Object.keys(overlayFields)),
|
||||
};
|
||||
this.rowDataCache[index] = row;
|
||||
} else {
|
||||
this.rowStatusCache[index] = {
|
||||
status: 'regular',
|
||||
};
|
||||
this.rowDataCache[index] = row;
|
||||
}
|
||||
this.rowCacheIndexes.add(index);
|
||||
}
|
||||
|
||||
getRowData(index: number) {
|
||||
this.requireRowCache(index);
|
||||
return this.rowDataCache[index];
|
||||
}
|
||||
|
||||
getRowStatus(index): GriderRowStatus {
|
||||
this.requireRowCache(index);
|
||||
return this.rowStatusCache[index];
|
||||
}
|
||||
|
||||
get rowCount() {
|
||||
return this.sourceRows.length + this.prependRows.length;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
<script lang="ts">
|
||||
import ShowFormButton from '../formview/ShowFormButton.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
export let rowIndex;
|
||||
export let onShowForm;
|
||||
|
||||
export let extraIcon = null;
|
||||
export let extraIconTooltip = null;
|
||||
|
||||
let mouseIn = false;
|
||||
</script>
|
||||
|
||||
@@ -18,6 +22,11 @@
|
||||
{#if mouseIn && onShowForm}
|
||||
<ShowFormButton on:click={onShowForm} />
|
||||
{/if}
|
||||
{#if extraIcon}
|
||||
<div class="extraIcon" title={extraIconTooltip}>
|
||||
<FontIcon icon={extraIcon} />
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<style>
|
||||
@@ -29,4 +38,9 @@
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.extraIcon {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import { openImportExportTab } from '../utility/importExportTools';
|
||||
import { getIntSettingsValue } from '../settings/settingsTools';
|
||||
import OverlayDiffGrider from './OverlayDiffGrider';
|
||||
|
||||
export let conid;
|
||||
export let display;
|
||||
@@ -92,6 +93,7 @@
|
||||
export let config;
|
||||
export let changeSetState;
|
||||
export let dispatchChangeSet;
|
||||
export let overlayDefinition = null;
|
||||
|
||||
export let macroPreview;
|
||||
export let macroValues;
|
||||
@@ -110,7 +112,7 @@
|
||||
// $: console.log('loadedRows BIND', loadedRows);
|
||||
|
||||
$: {
|
||||
if (macroPreview) {
|
||||
if (!overlayDefinition && macroPreview) {
|
||||
grider = new ChangeSetGrider(
|
||||
loadedRows,
|
||||
changeSetState,
|
||||
@@ -124,13 +126,25 @@
|
||||
}
|
||||
// prevent recreate grider, if no macro is selected, so there is no need to selectedcells in macro
|
||||
$: {
|
||||
if (!macroPreview) {
|
||||
if (!overlayDefinition && !macroPreview) {
|
||||
grider = new ChangeSetGrider(loadedRows, changeSetState, dispatchChangeSet, display);
|
||||
}
|
||||
}
|
||||
// $: console.log('GRIDER', grider);
|
||||
// $: if (onChangeGrider) onChangeGrider(grider);
|
||||
|
||||
$: {
|
||||
if (overlayDefinition) {
|
||||
grider = new OverlayDiffGrider(
|
||||
loadedRows,
|
||||
display,
|
||||
overlayDefinition.matchColumns,
|
||||
overlayDefinition.overlayData,
|
||||
overlayDefinition.matchedDbKeys
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function exportGrid() {
|
||||
const coninfo = await getConnectionInfo({ conid });
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@
|
||||
|
||||
export let isRawMode = false;
|
||||
|
||||
export let forceReadOnly = false;
|
||||
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: dbinfo = useDatabaseInfo({ conid, database });
|
||||
$: serverVersion = useDatabaseServerVersion({ conid, database });
|
||||
@@ -73,7 +75,7 @@
|
||||
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
||||
$serverVersion,
|
||||
table => getDictionaryDescription(table, conid, database, $apps, $connections),
|
||||
$connection?.isReadOnly,
|
||||
forceReadOnly || $connection?.isReadOnly,
|
||||
isRawMode
|
||||
)
|
||||
: null;
|
||||
@@ -161,7 +163,7 @@
|
||||
formViewComponent={SqlFormView}
|
||||
{display}
|
||||
showReferences
|
||||
showMacros={!$connection?.isReadOnly}
|
||||
showMacros={!forceReadOnly && !$connection?.isReadOnly}
|
||||
hasMultiColumnFilter
|
||||
onRunMacro={handleRunMacro}
|
||||
macroCondition={macro => macro.type == 'transformValue'}
|
||||
|
||||
@@ -69,6 +69,8 @@
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
leaflet.control.scale().addTo(map);
|
||||
|
||||
addObjectToMap();
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import Link from './Link.svelte';
|
||||
import TableControl from './TableControl.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let title;
|
||||
export let collection;
|
||||
@@ -12,6 +13,9 @@
|
||||
export let hideDisplayName = false;
|
||||
export let clickable = false;
|
||||
export let onAddNew = null;
|
||||
export let displayNameFieldName = null;
|
||||
|
||||
export let filters = writable({});
|
||||
|
||||
let collapsed = false;
|
||||
</script>
|
||||
@@ -43,14 +47,16 @@
|
||||
rows={collection || []}
|
||||
columns={_.compact([
|
||||
!hideDisplayName && {
|
||||
fieldName: 'displayName',
|
||||
fieldName: displayNameFieldName || 'displayName',
|
||||
header: 'Name',
|
||||
slot: -1,
|
||||
sortable: true,
|
||||
filterable: !!displayNameFieldName,
|
||||
},
|
||||
...columns,
|
||||
])}
|
||||
{clickable}
|
||||
{filters}
|
||||
on:clickrow
|
||||
>
|
||||
<svelte:fragment slot="-1" let:row let:col>
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
props?: any;
|
||||
formatter?: any;
|
||||
slot?: number;
|
||||
slotKey?: string;
|
||||
isHighlighted?: Function;
|
||||
sortable?: boolean;
|
||||
filterable?: boolean;
|
||||
filteredExpression?: (row: any) => string;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -19,12 +22,19 @@
|
||||
import keycodes from '../utility/keycodes';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import DataFilterControl from '../datagrid/DataFilterControl.svelte';
|
||||
import { evalFilterBehaviour } from 'dbgate-tools';
|
||||
import { evaluateCondition } from 'dbgate-sqltree';
|
||||
import { compileCompoudEvalCondition } from 'dbgate-filterparser';
|
||||
import { chevronExpandIcon } from '../icons/expandIcons';
|
||||
|
||||
export let columns: (TableControlColumn | false)[];
|
||||
export let rows;
|
||||
export let rows = null;
|
||||
export let groupedRows = null;
|
||||
export let focusOnCreate = false;
|
||||
export let selectable = false;
|
||||
export let selectedIndex = 0;
|
||||
export let selectedKey = null;
|
||||
export let clickable = false;
|
||||
export let disableFocusOutline = false;
|
||||
export let emptyMessage = null;
|
||||
@@ -35,7 +45,11 @@
|
||||
|
||||
export let checkedKeys = null;
|
||||
export let onSetCheckedKeys = null;
|
||||
export let extractCheckedKey = x => x.id;
|
||||
export let extractTableItemKey = x => x.id;
|
||||
export let itemSupportsCheckbox = x => true;
|
||||
export let filters = null;
|
||||
|
||||
export let selectionMode: 'index' | 'key' = 'index';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -46,19 +60,120 @@
|
||||
});
|
||||
|
||||
const handleKeyDown = event => {
|
||||
if (event.keyCode == keycodes.downArrow) {
|
||||
selectedIndex = Math.min(selectedIndex + 1, sortedRows.length - 1);
|
||||
const oldSelectedIndex =
|
||||
selectionMode == 'index' ? selectedIndex : _.findIndex(flatRowsShown, x => extractTableItemKey(x) == selectedKey);
|
||||
let newIndex = oldSelectedIndex;
|
||||
|
||||
switch (event.keyCode) {
|
||||
case keycodes.downArrow:
|
||||
newIndex = Math.min(newIndex + 1, flatRowsShown.length - 1);
|
||||
break;
|
||||
case keycodes.upArrow:
|
||||
newIndex = Math.max(0, newIndex - 1);
|
||||
break;
|
||||
case keycodes.home:
|
||||
newIndex = 0;
|
||||
break;
|
||||
case keycodes.end:
|
||||
newIndex = rows.length - 1;
|
||||
break;
|
||||
case keycodes.pageUp:
|
||||
newIndex -= 10;
|
||||
break;
|
||||
case keycodes.pageDown:
|
||||
newIndex += 10;
|
||||
break;
|
||||
}
|
||||
if (event.keyCode == keycodes.upArrow) {
|
||||
selectedIndex = Math.max(0, selectedIndex - 1);
|
||||
if (newIndex < 0) {
|
||||
newIndex = 0;
|
||||
}
|
||||
if (newIndex >= flatRowsShown.length) {
|
||||
newIndex = flatRowsShown.length - 1;
|
||||
}
|
||||
|
||||
if (clickable && oldSelectedIndex != newIndex) {
|
||||
event.preventDefault();
|
||||
domRows[newIndex]?.scrollIntoView();
|
||||
if (clickable) {
|
||||
dispatch('clickrow', flatRowsShown[newIndex]);
|
||||
}
|
||||
if (selectionMode == 'index') {
|
||||
selectedIndex = newIndex;
|
||||
} else {
|
||||
selectedKey = extractTableItemKey(flatRowsShown[newIndex]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function filterRows(grows, filters) {
|
||||
const condition = compileCompoudEvalCondition(filters);
|
||||
|
||||
if (!condition) return grows;
|
||||
|
||||
return grows
|
||||
.map(gitem => {
|
||||
return {
|
||||
group: gitem.group,
|
||||
rows: gitem.rows.filter(row => {
|
||||
const newrow = { ...row };
|
||||
for (const col of columnList) {
|
||||
if (col.filteredExpression) {
|
||||
newrow[col.fieldName] = col.filteredExpression(row);
|
||||
}
|
||||
}
|
||||
return evaluateCondition(condition, newrow);
|
||||
}),
|
||||
};
|
||||
})
|
||||
.filter(gitem => gitem.rows.length > 0);
|
||||
}
|
||||
|
||||
// function computeGroupedRows(array) {
|
||||
// if (!extractGroupName) {
|
||||
// return [{ label: null, rows: array }];
|
||||
// }
|
||||
// const res = [];
|
||||
// let lastGroupName = null;
|
||||
// let buildArray = [];
|
||||
// for (const item of array) {
|
||||
// const groupName = extractGroupName(item);
|
||||
// if (lastGroupName != groupName) {
|
||||
// if (buildArray.length > 0) {
|
||||
// res.push({ label: lastGroupName, rows: buildArray });
|
||||
// }
|
||||
// lastGroupName = groupName;
|
||||
// buildArray = [];
|
||||
// }
|
||||
// buildArray.push(item);
|
||||
// }
|
||||
// if (buildArray.length > 0) {
|
||||
// res.push({ label: lastGroupName, rows: buildArray });
|
||||
// }
|
||||
// }
|
||||
|
||||
let sortedByField = null;
|
||||
let sortOrderIsDesc = false;
|
||||
let collapsedGroupIndexes = [];
|
||||
let domRows = {};
|
||||
|
||||
$: sortedRowsTmp = sortedByField ? _.sortBy(rows || [], sortedByField) : rows;
|
||||
$: sortedRows = sortOrderIsDesc ? [...sortedRowsTmp].reverse() : sortedRowsTmp;
|
||||
$: rowsSource = groupedRows ? groupedRows : [{ group: null, rows }];
|
||||
|
||||
$: filteredRows = filters ? filterRows(rowsSource, $filters) : rowsSource;
|
||||
|
||||
$: sortedRows = sortedByField
|
||||
? filteredRows.map(gitem => {
|
||||
let res = _.sortBy(gitem.rows, sortedByField);
|
||||
if (sortOrderIsDesc) res = [...res].reverse();
|
||||
return { group: gitem.group, rows: res };
|
||||
})
|
||||
: filteredRows;
|
||||
|
||||
// $: console.log('sortedRows', sortedRows);
|
||||
|
||||
$: flatRowsShown = sortedRows.map(gitem => gitem.rows).flat();
|
||||
$: checkableFlatRowsShown = flatRowsShown.filter(x => itemSupportsCheckbox(x));
|
||||
|
||||
// $: groupedRows = computeGroupedRows(sortedRows);
|
||||
</script>
|
||||
|
||||
<table
|
||||
@@ -73,7 +188,17 @@
|
||||
<thead class:stickyHeader>
|
||||
<tr>
|
||||
{#if checkedKeys}
|
||||
<th></th>
|
||||
<th>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checkableFlatRowsShown.every(r => checkedKeys.includes(extractTableItemKey(r)))}
|
||||
data-testid="TableControl_selectAllCheckBox"
|
||||
on:change={e => {
|
||||
if (e.target['checked']) onSetCheckedKeys(checkableFlatRowsShown.map(r => extractTableItemKey(r)));
|
||||
else onSetCheckedKeys([]);
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
{/if}
|
||||
{#each columnList as col}
|
||||
<th
|
||||
@@ -101,60 +226,114 @@
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each sortedRows as row, index}
|
||||
<tr
|
||||
class:selected={selectable && selectedIndex == index}
|
||||
class:clickable
|
||||
on:click={() => {
|
||||
if (selectable) {
|
||||
selectedIndex = index;
|
||||
domTable.focus();
|
||||
}
|
||||
if (clickable) {
|
||||
dispatch('clickrow', row);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if filters}
|
||||
<tr>
|
||||
{#if checkedKeys}
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checkedKeys.includes(extractCheckedKey(row))}
|
||||
on:change={e => {
|
||||
if (e.target['checked']) onSetCheckedKeys(_.uniq([...checkedKeys, extractCheckedKey(row)]));
|
||||
else onSetCheckedKeys(checkedKeys.filter(x => x != extractCheckedKey(row)));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td class="empty-cell"></td>
|
||||
{/if}
|
||||
{#each columnList as col}
|
||||
{@const rowProps = { ...col.props, ...(col.getProps ? col.getProps(row) : null) }}
|
||||
<td class:isHighlighted={col.isHighlighted && col.isHighlighted(row)} class:noCellPadding>
|
||||
{#if col.component}
|
||||
<svelte:component this={col.component} {...rowProps} />
|
||||
{:else if col.formatter}
|
||||
{col.formatter(row)}
|
||||
{:else if col.slot != null}
|
||||
{#if col.slot == -1}<slot name="-1" {row} {col} {index} />
|
||||
{:else if col.slot == 0}<slot name="0" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 1}<slot name="1" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 2}<slot name="2" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 3}<slot name="3" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 4}<slot name="4" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 5}<slot name="5" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 6}<slot name="6" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 7}<slot name="7" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 8}<slot name="8" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 9}<slot name="9" {row} {col} {index} {...rowProps} />
|
||||
{/if}
|
||||
{:else}
|
||||
{row[col.fieldName] || ''}
|
||||
<td class="filter-cell" class:empty-cell={!col.filterable}>
|
||||
{#if col.filterable}
|
||||
<DataFilterControl
|
||||
filterBehaviour={evalFilterBehaviour}
|
||||
filter={$filters[col.fieldName]}
|
||||
setFilter={value => filters.update(f => ({ ...f, [col.fieldName]: value }))}
|
||||
placeholder="Data filter"
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/if}
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each sortedRows as gitem, groupIndex}
|
||||
{#if gitem.group}
|
||||
<tr class="group-row">
|
||||
<td
|
||||
colspan={columnList.length + (checkedKeys ? 1 : 0)}
|
||||
class="groupcell"
|
||||
on:click={() => {
|
||||
if (collapsedGroupIndexes.includes(groupIndex)) {
|
||||
collapsedGroupIndexes = collapsedGroupIndexes.filter(x => x != groupIndex);
|
||||
} else {
|
||||
collapsedGroupIndexes = [...collapsedGroupIndexes, groupIndex];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontIcon icon={chevronExpandIcon(!collapsedGroupIndexes.includes(groupIndex))} padRight />
|
||||
<strong>{gitem.group} ({gitem.rows.length})</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if !collapsedGroupIndexes.includes(groupIndex)}
|
||||
{#each gitem.rows as row}
|
||||
{@const index = _.indexOf(flatRowsShown, row)}
|
||||
<tr
|
||||
class:selected={selectable &&
|
||||
(selectionMode == 'index' ? selectedIndex == index : selectedKey == extractTableItemKey(row))}
|
||||
class:clickable
|
||||
bind:this={domRows[index]}
|
||||
on:click={() => {
|
||||
if (selectable) {
|
||||
if (selectionMode == 'index') {
|
||||
selectedIndex = index;
|
||||
} else {
|
||||
selectedKey = extractTableItemKey(row);
|
||||
}
|
||||
domTable.focus();
|
||||
}
|
||||
if (clickable) {
|
||||
dispatch('clickrow', row);
|
||||
}
|
||||
}}
|
||||
data-testid={`TableControl_row_${index}`}
|
||||
>
|
||||
{#if checkedKeys}
|
||||
<td>
|
||||
{#if itemSupportsCheckbox(row)}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checkedKeys.includes(extractTableItemKey(row))}
|
||||
on:change={e => {
|
||||
if (e.target['checked']) onSetCheckedKeys(_.uniq([...checkedKeys, extractTableItemKey(row)]));
|
||||
else onSetCheckedKeys(checkedKeys.filter(x => x != extractTableItemKey(row)));
|
||||
}}
|
||||
data-testid={`TableControl_row_${index}_checkbox`}
|
||||
/>
|
||||
{/if}
|
||||
</td>
|
||||
{/if}
|
||||
{#each columnList as col}
|
||||
{@const rowProps = { ...col.props, ...(col.getProps ? col.getProps(row) : null) }}
|
||||
<td class:isHighlighted={col.isHighlighted && col.isHighlighted(row)} class:noCellPadding>
|
||||
{#if col.component}
|
||||
<svelte:component this={col.component} {...rowProps} />
|
||||
{:else if col.formatter}
|
||||
{col.formatter(row)}
|
||||
{:else if col.slot != null}
|
||||
{#key row[col.slotKey] || 'key'}
|
||||
{#if col.slot == -1}<slot name="-1" {row} {col} {index} />
|
||||
{:else if col.slot == 0}<slot name="0" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 1}<slot name="1" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 2}<slot name="2" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 3}<slot name="3" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 4}<slot name="4" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 5}<slot name="5" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 6}<slot name="6" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 7}<slot name="7" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 8}<slot name="8" {row} {col} {index} {...rowProps} />
|
||||
{:else if col.slot == 9}<slot name="9" {row} {col} {index} {...rowProps} />
|
||||
{/if}
|
||||
{/key}
|
||||
{:else}
|
||||
{row[col.fieldName] || ''}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
{#if emptyMessage && sortedRows.length == 0}
|
||||
<tr>
|
||||
@@ -179,6 +358,9 @@
|
||||
background: var(--theme-bg-0);
|
||||
}
|
||||
tbody tr.selected {
|
||||
background: var(--theme-bg-3);
|
||||
}
|
||||
table:focus tbody tr.selected {
|
||||
background: var(--theme-bg-selected);
|
||||
}
|
||||
tbody tr.clickable:hover {
|
||||
@@ -232,4 +414,20 @@
|
||||
border-collapse: separate;
|
||||
border-left: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
.filter-cell {
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.empty-cell {
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
|
||||
.groupcell {
|
||||
background-color: var(--theme-bg-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,14 +10,17 @@
|
||||
|
||||
export let folderName;
|
||||
export let name;
|
||||
export let filterExtension = null;
|
||||
|
||||
const { setFieldValue, values } = getFormContext();
|
||||
|
||||
$: files = useArchiveFiles({ folder: folderName });
|
||||
$: filesOptions = ($files || []).map(x => ({
|
||||
value: x.name,
|
||||
label: x.name,
|
||||
}));
|
||||
$: filesOptions = ($files || [])
|
||||
.filter(x => (filterExtension ? x.name.endsWith('.' + filterExtension) : true))
|
||||
.map(x => ({
|
||||
value: x.name,
|
||||
label: x.name,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
@@ -11,16 +11,22 @@
|
||||
|
||||
export let additionalFolders = [];
|
||||
export let name;
|
||||
export let allowCreateNew = false;
|
||||
export let zipFilesOnly = false;
|
||||
export let skipZipFiles = false;
|
||||
|
||||
const { setFieldValue } = getFormContext();
|
||||
|
||||
const folders = useArchiveFolders();
|
||||
|
||||
$: folderOptions = [
|
||||
...($folders || []).map(folder => ({
|
||||
value: folder.name,
|
||||
label: folder.name,
|
||||
})),
|
||||
...($folders || [])
|
||||
.filter(folder => (zipFilesOnly ? folder.name.endsWith('.zip') : true))
|
||||
.filter(folder => (skipZipFiles ? !folder.name.endsWith('.zip') : true))
|
||||
.map(folder => ({
|
||||
value: folder.name,
|
||||
label: folder.name,
|
||||
})),
|
||||
...additionalFolders
|
||||
.filter(x => x != '@create')
|
||||
.filter(x => !($folders || []).find(y => y.name == x))
|
||||
@@ -28,7 +34,7 @@
|
||||
value: folder,
|
||||
label: folder,
|
||||
})),
|
||||
{
|
||||
allowCreateNew && {
|
||||
label: '(Create new)',
|
||||
value: '@create',
|
||||
},
|
||||
@@ -43,7 +49,7 @@
|
||||
if (e.detail == '@create') {
|
||||
showModal(InputTextModal, {
|
||||
header: 'Archive',
|
||||
label: 'Name of new folder',
|
||||
label: 'Name of new archive folder',
|
||||
onConfirm: createOption,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
? { disabled: true }
|
||||
: {
|
||||
onClick: () => {
|
||||
setFieldValue(name, !$values[name]);
|
||||
setFieldValue(name, $values?.[name] == 0 ? true : $values?.[name] == 1 ? false : !$values?.[name]);
|
||||
dispatch('change');
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -11,4 +11,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<CheckboxField {...$$restProps} checked={$values[name] ?? defaultValue} on:change={handleChange} on:change />
|
||||
<CheckboxField
|
||||
{...$$restProps}
|
||||
checked={$values?.[name] == 0 ? false : $values?.[name] == '1' ? true : ($values?.[name] ?? defaultValue)}
|
||||
on:change={handleChange}
|
||||
on:change
|
||||
/>
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
'icon history': 'mdi mdi-history',
|
||||
'icon structure': 'mdi mdi-tools',
|
||||
'icon square': 'mdi mdi-square',
|
||||
'icon data-deploy': 'mdi mdi-database-settings',
|
||||
|
||||
'icon edit': 'mdi mdi-pencil',
|
||||
'icon delete': 'mdi mdi-delete',
|
||||
@@ -206,6 +207,8 @@
|
||||
'icon type-objectid': 'mdi mdi-alpha-i-box',
|
||||
'icon type-null': 'mdi mdi-code-equal',
|
||||
'icon type-unknown': 'mdi mdi-help-box',
|
||||
'icon equal': 'mdi mdi-equal',
|
||||
'icon not-equal': 'mdi mdi-not-equal-variant',
|
||||
|
||||
'icon at': 'mdi mdi-at',
|
||||
'icon expand-all': 'mdi mdi-expand-all',
|
||||
@@ -218,6 +221,7 @@
|
||||
'icon autocommit-off': 'mdi mdi-check-circle-outline',
|
||||
|
||||
'icon premium': 'mdi mdi-star',
|
||||
'icon upload': 'mdi mdi-upload',
|
||||
|
||||
'img ok': 'mdi mdi-check-circle color-icon-green',
|
||||
'img ok-inv': 'mdi mdi-check-circle color-icon-inv-green',
|
||||
@@ -232,12 +236,14 @@
|
||||
|
||||
'img archive': 'mdi mdi-table color-icon-gold',
|
||||
'img archive-folder': 'mdi mdi-database-outline color-icon-green',
|
||||
'img zipfile': 'mdi mdi-zip-box color-icon-gold',
|
||||
'img autoincrement': 'mdi mdi-numeric-1-box-multiple-outline',
|
||||
'img column': 'mdi mdi-table-column',
|
||||
'img server': 'mdi mdi-server color-icon-blue',
|
||||
'img primary-key': 'mdi mdi-key-star color-icon-yellow',
|
||||
'img foreign-key': 'mdi mdi-key-link',
|
||||
'img sql-file': 'mdi mdi-file',
|
||||
'img anyfile': 'mdi mdi-file-question color-icon-red',
|
||||
'img shell': 'mdi mdi-flash color-icon-blue',
|
||||
'img chart': 'mdi mdi-chart-bar color-icon-magenta',
|
||||
'img markdown': 'mdi mdi-application color-icon-red',
|
||||
@@ -301,7 +307,7 @@
|
||||
'img type-rejson': 'mdi mdi-color-json color-icon-blue',
|
||||
'img keydb': 'mdi mdi-key color-icon-blue',
|
||||
|
||||
'img duplicator': 'mdi mdi-content-duplicate color-icon-green',
|
||||
'img replicator': 'mdi mdi-content-duplicate color-icon-green',
|
||||
'img import': 'mdi mdi-database-import color-icon-green',
|
||||
'img export': 'mdi mdi-database-export color-icon-green',
|
||||
'img transform': 'mdi mdi-rotate-orbit color-icon-blue',
|
||||
@@ -311,6 +317,8 @@
|
||||
|
||||
'img db-backup': 'mdi mdi-database-export color-icon-yellow',
|
||||
'img db-restore': 'mdi mdi-database-import color-icon-red',
|
||||
'img settings': 'mdi mdi-cog color-icon-blue',
|
||||
'img data-deploy': 'mdi mdi-database-settings color-icon-green',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { getConnectionLabel } from 'dbgate-tools';
|
||||
|
||||
export let allowChooseModel = false;
|
||||
export let direction;
|
||||
export let direction = 'source';
|
||||
|
||||
$: connections = useConnectionList();
|
||||
$: connectionOptions = [
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import AceEditor from '../query/AceEditor.svelte';
|
||||
import { _t } from '../translations';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
|
||||
export let direction;
|
||||
export let storageTypeField;
|
||||
@@ -54,7 +59,7 @@
|
||||
{ value: 'query', label: _t('common.query', { defaultMessage: 'Query' }), directions: ['source'] },
|
||||
{
|
||||
value: 'archive',
|
||||
label: _t('common.archive', { defaultMessage: 'Archive' }),
|
||||
label: _t('common.archive', { defaultMessage: 'Archive (JSONL)' }),
|
||||
directions: ['source', 'target'],
|
||||
},
|
||||
];
|
||||
@@ -108,11 +113,18 @@
|
||||
<FormStyledButton
|
||||
value="New archive"
|
||||
on:click={() => {
|
||||
values.update(x => ({
|
||||
...x,
|
||||
[storageTypeField]: 'archive',
|
||||
[archiveFolderField]: `import-${moment().format('YYYY-MM-DD-hh-mm-ss')}`,
|
||||
}));
|
||||
showModal(InputTextModal, {
|
||||
header: 'Archive',
|
||||
label: 'Name of new archive folder',
|
||||
value: `import-${moment().format('YYYY-MM-DD-hh-mm-ss')}`,
|
||||
onConfirm: value => {
|
||||
values.update(x => ({
|
||||
...x,
|
||||
[storageTypeField]: 'archive',
|
||||
[archiveFolderField]: value,
|
||||
}));
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -124,6 +136,41 @@
|
||||
label="Storage type"
|
||||
/>
|
||||
|
||||
{#if format && isProApp()}
|
||||
{#if direction == 'source'}
|
||||
<FormCheckboxField
|
||||
name={`importFromZipFile`}
|
||||
label={_t('importExport.importFromZipFile', { defaultMessage: 'Import from ZIP file (in archive folder)' })}
|
||||
/>
|
||||
{#if $values.importFromZipFile}
|
||||
<FormArchiveFolderSelect
|
||||
label={_t('importExport.importFromZipArchive', { defaultMessage: 'Input ZIP archive' })}
|
||||
name={archiveFolderField}
|
||||
additionalFolders={_.compact([$values[archiveFolderField]])}
|
||||
zipFilesOnly
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if direction == 'target'}
|
||||
<FormCheckboxField
|
||||
name={`exportToZipFile`}
|
||||
label={_t('importExport.exportToZipFile', { defaultMessage: 'Export to ZIP file' })}
|
||||
/>
|
||||
{#if $values.exportToZipFile}
|
||||
<FormCheckboxField
|
||||
name={`createZipFileInArchive`}
|
||||
label={_t('importExport.createZipFileInArchive', { defaultMessage: 'Create ZIP file in archive' })}
|
||||
/>
|
||||
|
||||
<FormTextField
|
||||
label={_t('importExport.exportToZipArchive', { defaultMessage: 'Output ZIP archive' })}
|
||||
name={archiveFolderField}
|
||||
placeholder={'zip-archive-yyyy-mm-dd-hh-mm-ss.zip'}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if storageType == 'database' || storageType == 'query'}
|
||||
<FormConnectionSelect name={connectionIdField} label="Server" {direction} />
|
||||
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} label="Database" />
|
||||
@@ -164,18 +211,20 @@
|
||||
label="Archive folder"
|
||||
name={archiveFolderField}
|
||||
additionalFolders={_.compact([$values[archiveFolderField]])}
|
||||
allowCreateNew={direction == 'target'}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if storageType == 'archive' && direction == 'source'}
|
||||
{#if direction == 'source' && (storageType == 'archive' || $values.importFromZipFile)}
|
||||
<FormArchiveFilesSelect
|
||||
label={_t('importExport.sourceFiles', { defaultMessage: 'Source files' })}
|
||||
folderName={$values[archiveFolderField]}
|
||||
name={tablesField}
|
||||
filterExtension={format?.extension}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if format && direction == 'source'}
|
||||
{#if format && direction == 'source' && !$values.importFromZipFile}
|
||||
<FilesInput {setPreviewSource} />
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { ScriptWriter, ScriptWriterJson } from 'dbgate-tools';
|
||||
import getAsArray from '../utility/getAsArray';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
@@ -93,7 +94,13 @@ function getSourceExpr(extensions, sourceName, values, sourceConnection, sourceD
|
||||
return [
|
||||
format.readerFunc,
|
||||
{
|
||||
..._.omit(sourceFile, ['isDownload']),
|
||||
...(sourceFile
|
||||
? _.omit(sourceFile, ['isDownload'])
|
||||
: {
|
||||
fileName: values.importFromZipFile
|
||||
? `zip://archive:${values.sourceArchiveFolder}//${sourceName}`
|
||||
: sourceName,
|
||||
}),
|
||||
...extractFormatApiParameters(values, 'source', format),
|
||||
},
|
||||
];
|
||||
@@ -237,6 +244,13 @@ export default async function createImpExpScript(extensions, values, forceScript
|
||||
script.copyStream(sourceVar, targetVar, colmapVar, sourceName);
|
||||
script.endLine();
|
||||
}
|
||||
|
||||
if (values.exportToZipFile) {
|
||||
let zipFileName = values.exportToZipFileName || `zip-archive-${moment().format('YYYY-MM-DD-HH-mm-ss')}.zip`;
|
||||
if (!zipFileName.endsWith('.zip')) zipFileName += '.zip';
|
||||
script.zipDirectory('.', values.createZipFileInArchive ? 'archive:' + zipFileName : zipFileName);
|
||||
}
|
||||
|
||||
return script.getScript(values.schedule);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<div>{message}</div>
|
||||
|
||||
<FormArchiveFolderSelect label="Archive folder" name="archiveFolder" isNative />
|
||||
<FormArchiveFolderSelect label="Archive folder" name="archiveFolder" isNative allowCreateNew />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<FormSubmit
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
<FormProvider>
|
||||
<ModalBase {...$$restProps}>
|
||||
<svelte:fragment slot="header">
|
||||
{#if header}
|
||||
{header}
|
||||
{/if}
|
||||
{header || 'Confirm'}
|
||||
</svelte:fragment>
|
||||
|
||||
{message}
|
||||
|
||||
366
packages/web/src/modals/ExportImportConnectionsModal.svelte
Normal file
366
packages/web/src/modals/ExportImportConnectionsModal.svelte
Normal file
@@ -0,0 +1,366 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||
import FormProvider from '../forms/FormProvider.svelte';
|
||||
|
||||
import ModalBase from './ModalBase.svelte';
|
||||
import { closeCurrentModal } from './modalTools';
|
||||
import { _t } from '../translations';
|
||||
import { apiCall } from '../utility/api';
|
||||
import TabControl from '../elements/TabControl.svelte';
|
||||
import TableControl from '../elements/TableControl.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import LargeButton from '../buttons/LargeButton.svelte';
|
||||
import { downloadFromApi } from '../utility/exportFileTools';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { showSnackbarSuccess } from '../utility/snackbar';
|
||||
import { format } from 'date-fns';
|
||||
import Link from '../elements/Link.svelte';
|
||||
import _ from 'lodash';
|
||||
|
||||
export let mode: 'export' | 'import';
|
||||
export let uploadedFilePath = undefined;
|
||||
|
||||
let fullData: any = {};
|
||||
|
||||
async function loadExportedData() {
|
||||
fullData = await apiCall('config/export-connections-and-settings');
|
||||
initFromFullData();
|
||||
}
|
||||
|
||||
async function loadImportedData() {
|
||||
fullData = await apiCall('files/get-jsons-from-zip', { filePath: uploadedFilePath });
|
||||
initFromFullData();
|
||||
}
|
||||
|
||||
function initFromFullData() {
|
||||
connections = fullData.connections || [];
|
||||
users = fullData.users || [];
|
||||
roles = fullData.roles || [];
|
||||
authMethods = fullData.auth_methods || [];
|
||||
config = fullData.config || [];
|
||||
|
||||
handleCheckAll(true);
|
||||
}
|
||||
|
||||
function handleCheckAll(checked) {
|
||||
if (checked) {
|
||||
checkedConnections = connections.map(x => x.id);
|
||||
checkedUsers = users.map(x => x.id);
|
||||
checkedRoles = roles.map(x => x.id);
|
||||
checkedAuthMethods = authMethods.map(x => x.id);
|
||||
checkedConfig = config.map(x => x.id);
|
||||
} else {
|
||||
checkedConnections = [];
|
||||
checkedUsers = [];
|
||||
checkedRoles = [];
|
||||
checkedAuthMethods = [];
|
||||
checkedConfig = [];
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (mode == 'export') {
|
||||
loadExportedData();
|
||||
}
|
||||
if (mode == 'import') {
|
||||
loadImportedData();
|
||||
}
|
||||
});
|
||||
|
||||
function getLimitedData() {
|
||||
const limitedData: any = {
|
||||
connections: fullData.connections?.filter(x => checkedConnections.includes(x.id)),
|
||||
|
||||
users: fullData.users?.filter(x => checkedUsers.includes(x.id)),
|
||||
|
||||
user_connections: fullData.user_connections?.filter(
|
||||
x => checkedUsers.includes(x.user_id) && checkedConnections.includes(x.connection_id)
|
||||
),
|
||||
user_roles: fullData.user_roles?.filter(x => checkedUsers.includes(x.user_id) && checkedRoles.includes(x.role_id)),
|
||||
user_permissions: fullData.user_permissions?.filter(x => checkedUsers.includes(x.user_id)),
|
||||
|
||||
roles: fullData.roles?.filter(x => checkedRoles.includes(x.id)),
|
||||
role_connections: fullData.role_connections?.filter(
|
||||
x => checkedRoles.includes(x.role_id) && checkedConnections.includes(x.connection_id)
|
||||
),
|
||||
role_permissions: fullData.role_permissions?.filter(x => checkedRoles.includes(x.role_id)),
|
||||
|
||||
auth_methods: fullData.auth_methods?.filter(x => checkedAuthMethods.includes(x.id)),
|
||||
auth_methods_config: fullData.auth_methods_config?.filter(x => checkedAuthMethods.includes(x.auth_method_id)),
|
||||
|
||||
config: fullData.config?.filter(
|
||||
x => checkedConfig.includes(x.id) || (x.group == 'admin' && x.key == 'encryptionKey')
|
||||
),
|
||||
};
|
||||
return limitedData;
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
const electron = getElectron();
|
||||
|
||||
let filePath;
|
||||
let fileName;
|
||||
|
||||
if (electron) {
|
||||
const electron = getElectron();
|
||||
filePath = await electron.showSaveDialog({
|
||||
filters: [
|
||||
{ name: `ZIP files`, extensions: ['zip'] },
|
||||
{ name: `All files`, extensions: ['*'] },
|
||||
],
|
||||
defaultPath: `dbgateconfig.zip`,
|
||||
properties: ['showOverwriteConfirmation'],
|
||||
});
|
||||
} else {
|
||||
const resp = await apiCall('files/generate-uploads-file', { extension: 'sql' });
|
||||
filePath = resp.filePath;
|
||||
fileName = resp.fileName;
|
||||
}
|
||||
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
await apiCall('files/create-zip-from-jsons', { db: getLimitedData(), filePath });
|
||||
|
||||
if (electron) {
|
||||
showSnackbarSuccess(`Saved to file ${filePath}`);
|
||||
} else {
|
||||
await downloadFromApi(`uploads/get?file=${fileName}`, `dbgateconfig.zip`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveToArchive() {
|
||||
const filePath = `archive:dbgateconfig-${format(new Date(), 'yyyy-MM-dd-HH-mm-ss')}.zip`;
|
||||
await apiCall('files/create-zip-from-jsons', { db: getLimitedData(), filePath });
|
||||
showSnackbarSuccess(`Saved to ${filePath}`);
|
||||
}
|
||||
|
||||
async function handleImport() {
|
||||
await apiCall('config/import-connections-and-settings', { db: getLimitedData() });
|
||||
showSnackbarSuccess(`Imported connections and settings`);
|
||||
}
|
||||
|
||||
let connections = [];
|
||||
let checkedConnections = [];
|
||||
|
||||
let users = [];
|
||||
let checkedUsers = [];
|
||||
|
||||
let roles = [];
|
||||
let checkedRoles = [];
|
||||
|
||||
let authMethods = [];
|
||||
let checkedAuthMethods = [];
|
||||
|
||||
let config = [];
|
||||
let checkedConfig = [];
|
||||
|
||||
const connectionFilters = writable({});
|
||||
const userFilters = writable({});
|
||||
const roleFilters = writable({});
|
||||
const authMethodFilters = writable({});
|
||||
const configFilters = writable({});
|
||||
</script>
|
||||
|
||||
<FormProvider>
|
||||
<ModalBase {...$$restProps} fullScreen>
|
||||
<div slot="header">
|
||||
{mode == 'export' ? 'Export' : 'Import'} connections & settings
|
||||
<span class="check-uncheck">
|
||||
<Link onClick={() => handleCheckAll(true)}>Check all</Link>
|
||||
|
|
||||
<Link onClick={() => handleCheckAll(false)}>Uncheck all</Link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<TabControl
|
||||
tabs={_.compact([
|
||||
connections?.length && {
|
||||
label: `Connections (${checkedConnections?.length}/${connections?.length})`,
|
||||
slot: 1,
|
||||
},
|
||||
users?.length && { label: `Users (${checkedUsers?.length}/${users?.length})`, slot: 2 },
|
||||
roles?.length && { label: `Roles (${checkedRoles?.length}/${roles?.length})`, slot: 3 },
|
||||
authMethods?.length && {
|
||||
label: `Auth methods (${checkedAuthMethods?.length}/${authMethods?.length})`,
|
||||
slot: 4,
|
||||
},
|
||||
config?.length && { label: `Config (${checkedConfig?.length}/${config?.length})`, slot: 5 },
|
||||
])}
|
||||
>
|
||||
<svelte:fragment slot="1">
|
||||
<div class="tablewrap">
|
||||
<TableControl
|
||||
filters={connectionFilters}
|
||||
stickyHeader
|
||||
columns={[
|
||||
{ header: 'ID', fieldName: 'id', sortable: true, filterable: true },
|
||||
{ header: 'Display name', fieldName: 'displayName', sortable: true, filterable: true },
|
||||
{ header: 'Engine', fieldName: 'engine', sortable: true, filterable: true },
|
||||
{ header: 'Server', fieldName: 'server', sortable: true, filterable: true },
|
||||
{ header: 'User', fieldName: 'user', sortable: true, filterable: true },
|
||||
]}
|
||||
clickable
|
||||
rows={connections}
|
||||
on:clickrow={event => {
|
||||
checkedConnections = checkedConnections.includes(event.detail.id)
|
||||
? checkedConnections.filter(id => id !== event.detail.id)
|
||||
: [...checkedConnections, event.detail.id];
|
||||
}}
|
||||
checkedKeys={checkedConnections}
|
||||
onSetCheckedKeys={keys => {
|
||||
checkedConnections = keys;
|
||||
}}
|
||||
></TableControl>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="2">
|
||||
<div class="tablewrap">
|
||||
<TableControl
|
||||
filters={userFilters}
|
||||
stickyHeader
|
||||
columns={[
|
||||
{ header: 'ID', fieldName: 'id', sortable: true, filterable: true },
|
||||
{ header: 'Login', fieldName: 'login', sortable: true, filterable: true },
|
||||
{ header: 'E-mail', fieldName: 'email', sortable: true, filterable: true },
|
||||
]}
|
||||
clickable
|
||||
rows={users}
|
||||
on:clickrow={event => {
|
||||
checkedUsers = checkedUsers.includes(event.detail.id)
|
||||
? checkedUsers.filter(id => id !== event.detail.id)
|
||||
: [...checkedUsers, event.detail.id];
|
||||
}}
|
||||
checkedKeys={checkedUsers}
|
||||
onSetCheckedKeys={keys => {
|
||||
checkedUsers = keys;
|
||||
}}
|
||||
></TableControl>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="3">
|
||||
<div class="tablewrap">
|
||||
<TableControl
|
||||
filters={roleFilters}
|
||||
stickyHeader
|
||||
columns={[
|
||||
{ header: 'ID', fieldName: 'id', sortable: true, filterable: true },
|
||||
{ header: 'Name', fieldName: 'name', sortable: true, filterable: true },
|
||||
]}
|
||||
clickable
|
||||
rows={roles}
|
||||
on:clickrow={event => {
|
||||
checkedRoles = checkedRoles.includes(event.detail.id)
|
||||
? checkedRoles.filter(id => id !== event.detail.id)
|
||||
: [...checkedRoles, event.detail.id];
|
||||
}}
|
||||
checkedKeys={checkedRoles}
|
||||
onSetCheckedKeys={keys => {
|
||||
checkedRoles = keys;
|
||||
}}
|
||||
></TableControl>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="4">
|
||||
<div class="tablewrap">
|
||||
<TableControl
|
||||
filters={authMethodFilters}
|
||||
stickyHeader
|
||||
columns={[
|
||||
{ header: 'ID', fieldName: 'id', sortable: true, filterable: true },
|
||||
{ header: 'Name', fieldName: 'name', sortable: true, filterable: true },
|
||||
{ header: 'Type', fieldName: 'type', sortable: true, filterable: true },
|
||||
]}
|
||||
clickable
|
||||
rows={authMethods}
|
||||
on:clickrow={event => {
|
||||
checkedAuthMethods = checkedAuthMethods.includes(event.detail.id)
|
||||
? checkedAuthMethods.filter(id => id !== event.detail.id)
|
||||
: [...checkedAuthMethods, event.detail.id];
|
||||
}}
|
||||
checkedKeys={checkedAuthMethods}
|
||||
onSetCheckedKeys={keys => {
|
||||
checkedAuthMethods = keys;
|
||||
}}
|
||||
></TableControl>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="5">
|
||||
<div class="tablewrap">
|
||||
<TableControl
|
||||
filters={configFilters}
|
||||
stickyHeader
|
||||
columns={[
|
||||
{ header: 'ID', fieldName: 'id', sortable: true, filterable: true },
|
||||
{ header: 'Group', fieldName: 'group', sortable: true, filterable: true },
|
||||
{ header: 'Key', fieldName: 'key', sortable: true, filterable: true },
|
||||
{ header: 'Value', fieldName: 'value', sortable: true, filterable: true },
|
||||
]}
|
||||
clickable
|
||||
rows={config}
|
||||
on:clickrow={event => {
|
||||
checkedConfig = checkedConfig.includes(event.detail.id)
|
||||
? checkedConfig.filter(id => id !== event.detail.id)
|
||||
: [...checkedConfig, event.detail.id];
|
||||
}}
|
||||
checkedKeys={checkedConfig}
|
||||
onSetCheckedKeys={keys => {
|
||||
checkedConfig = keys;
|
||||
}}
|
||||
></TableControl>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</TabControl>
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<div class="flex m-2">
|
||||
{#if mode == 'export'}
|
||||
<LargeButton
|
||||
data-testid="ExportImportConnectionsModal_exportButton"
|
||||
icon="icon export"
|
||||
on:click={handleExport}>{_t('common.export', { defaultMessage: 'Export' })}</LargeButton
|
||||
>
|
||||
<LargeButton
|
||||
data-testid="ExportImportConnectionsModal_saveToArchive"
|
||||
icon="icon archive"
|
||||
on:click={handleSaveToArchive}
|
||||
>{_t('common.saveToArchive', { defaultMessage: 'Save to archive' })}</LargeButton
|
||||
>
|
||||
{/if}
|
||||
{#if mode == 'import'}
|
||||
<LargeButton
|
||||
data-testid="ExportImportConnectionsModal_importButton"
|
||||
icon="icon import"
|
||||
on:click={handleImport}>{_t('common.import', { defaultMessage: 'Import' })}</LargeButton
|
||||
>
|
||||
{/if}
|
||||
<LargeButton icon="icon close" on:click={closeCurrentModal} data-testid="EditJsonModal_closeButton"
|
||||
>Close</LargeButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBase>
|
||||
</FormProvider>
|
||||
|
||||
<style>
|
||||
.tablewrap {
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: calc(100vh - 220px);
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-uncheck {
|
||||
margin-left: 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
@@ -13,6 +13,7 @@
|
||||
export let file = 'new-table';
|
||||
export let folder = $currentArchive;
|
||||
export let onSave;
|
||||
export let fileIsReadOnly = false;
|
||||
|
||||
const handleSubmit = async e => {
|
||||
const { file, folder } = e.detail;
|
||||
@@ -25,8 +26,8 @@
|
||||
<ModalBase {...$$restProps}>
|
||||
<svelte:fragment slot="header">Save to archive</svelte:fragment>
|
||||
|
||||
<FormArchiveFolderSelect label="Folder" name="folder" isNative />
|
||||
<FormTextField label="File name" name="file" />
|
||||
<FormArchiveFolderSelect label="Folder" name="folder" isNative allowCreateNew skipZipFiles />
|
||||
<FormTextField label="File name" name="file" disabled={fileIsReadOnly} />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<FormSubmit value={_t('common.save', { defaultMessage: 'Save' })} on:click={handleSubmit} />
|
||||
|
||||
@@ -42,60 +42,63 @@
|
||||
{#if !files || files.length == 0}
|
||||
<ErrorInfo message="No output files" icon="img alert" />
|
||||
{:else}
|
||||
<TableControl
|
||||
rows={files}
|
||||
columns={[
|
||||
{ fieldName: 'name', header: 'Name' },
|
||||
{ fieldName: 'size', header: 'Size', formatter: row => formatFileSize(row.size) },
|
||||
!electron && {
|
||||
fieldName: 'download',
|
||||
header: 'Download',
|
||||
slot: 0,
|
||||
},
|
||||
electron && {
|
||||
fieldName: 'copy',
|
||||
header: 'Copy',
|
||||
slot: 1,
|
||||
},
|
||||
electron && {
|
||||
fieldName: 'show',
|
||||
header: 'Show',
|
||||
slot: 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="0" let:row>
|
||||
<Link
|
||||
onClick={() => {
|
||||
downloadFromApi(`runners/data/${runnerId}/${row.name}`, row.name);
|
||||
}}
|
||||
>
|
||||
download
|
||||
</Link>
|
||||
</svelte:fragment>
|
||||
<div class="flex1 scroll">
|
||||
<TableControl
|
||||
rows={files}
|
||||
stickyHeader
|
||||
columns={[
|
||||
{ fieldName: 'name', header: 'Name' },
|
||||
{ fieldName: 'size', header: 'Size', formatter: row => formatFileSize(row.size) },
|
||||
!electron && {
|
||||
fieldName: 'download',
|
||||
header: 'Download',
|
||||
slot: 0,
|
||||
},
|
||||
electron && {
|
||||
fieldName: 'copy',
|
||||
header: 'Copy',
|
||||
slot: 1,
|
||||
},
|
||||
electron && {
|
||||
fieldName: 'show',
|
||||
header: 'Show',
|
||||
slot: 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="0" let:row>
|
||||
<Link
|
||||
onClick={() => {
|
||||
downloadFromApi(`runners/data/${runnerId}/${row.name}`, row.name);
|
||||
}}
|
||||
>
|
||||
download
|
||||
</Link>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="1" let:row>
|
||||
<Link
|
||||
onClick={async () => {
|
||||
const file = await electron.showSaveDialog({});
|
||||
if (file) {
|
||||
const fs = window.require('fs');
|
||||
fs.copyFile(row.path, file, () => {});
|
||||
}
|
||||
}}
|
||||
>
|
||||
save
|
||||
</Link>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="1" let:row>
|
||||
<Link
|
||||
onClick={async () => {
|
||||
const file = await electron.showSaveDialog({});
|
||||
if (file) {
|
||||
const fs = window.require('fs');
|
||||
fs.copyFile(row.path, file, () => {});
|
||||
}
|
||||
}}
|
||||
>
|
||||
save
|
||||
</Link>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="2" let:row>
|
||||
<Link
|
||||
onClick={() => {
|
||||
electron.showItemInFolder(row.path);
|
||||
}}
|
||||
>
|
||||
show
|
||||
</Link>
|
||||
</svelte:fragment>
|
||||
</TableControl>
|
||||
<svelte:fragment slot="2" let:row>
|
||||
<Link
|
||||
onClick={() => {
|
||||
electron.showItemInFolder(row.path);
|
||||
}}
|
||||
>
|
||||
show
|
||||
</Link>
|
||||
</svelte:fragment>
|
||||
</TableControl>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -433,7 +433,7 @@ ORDER BY
|
||||
|
||||
<FormCheckboxField
|
||||
name="behaviour.jsonPreviewWrap"
|
||||
label={_t('settings.behaviour.jsonPreviewWrap', { defaultMessage: 'Wrap json in preview' })}
|
||||
label={_t('settings.behaviour.jsonPreviewWrap', { defaultMessage: 'Wrap JSON in preview' })}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
|
||||
@@ -192,6 +192,7 @@
|
||||
clickable
|
||||
on:clickrow={e => showModal(ColumnEditorModal, { columnInfo: e.detail, tableInfo, setTableInfo, driver })}
|
||||
onAddNew={isWritable ? addColumn : null}
|
||||
displayNameFieldName="columnName"
|
||||
columns={[
|
||||
!driver?.dialect?.specificNullabilityImplementation && {
|
||||
fieldName: 'notNull',
|
||||
@@ -203,11 +204,13 @@
|
||||
fieldName: 'dataType',
|
||||
header: 'Data Type',
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'defaultValue',
|
||||
header: 'Default value',
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
},
|
||||
driver?.dialect?.columnProperties?.isSparse && {
|
||||
fieldName: 'isSparse',
|
||||
@@ -219,6 +222,7 @@
|
||||
fieldName: 'computedExpression',
|
||||
header: 'Computed Expression',
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
},
|
||||
driver?.dialect?.columnProperties?.isPersisted && {
|
||||
fieldName: 'isPersisted',
|
||||
@@ -242,10 +246,12 @@
|
||||
fieldName: 'columnComment',
|
||||
header: 'Comment',
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
},
|
||||
isWritable
|
||||
? {
|
||||
fieldName: 'actions',
|
||||
filterable: false,
|
||||
slot: 3,
|
||||
}
|
||||
: null,
|
||||
|
||||
@@ -57,8 +57,10 @@
|
||||
export let jslid = undefined;
|
||||
|
||||
export let tabid;
|
||||
|
||||
let infoLoadCounter = 0;
|
||||
let jslidChecked = false;
|
||||
let extractedJslId = null;
|
||||
|
||||
const quickExportHandlerRef = createQuickExportHandlerRef();
|
||||
|
||||
@@ -155,6 +157,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (archiveFolder?.endsWith('.zip')) {
|
||||
const resp = await apiCall('jsldata/download-jsl-data', {
|
||||
uri: `zip://archive:${archiveFolder}//${archiveFile}.jsonl`,
|
||||
});
|
||||
extractedJslId = resp.jslid;
|
||||
}
|
||||
|
||||
jslidChecked = true;
|
||||
}
|
||||
|
||||
@@ -166,7 +176,7 @@
|
||||
<ToolStripContainer>
|
||||
{#if jslidChecked || !jslid}
|
||||
<JslDataGrid
|
||||
jslid={jslid || `archive://${archiveFolder}/${archiveFile}`}
|
||||
jslid={extractedJslId || jslid || `archive://${archiveFolder}/${archiveFile}`}
|
||||
supportsReload
|
||||
allowChangeChangeSetStructure
|
||||
changeSetState={$changeSetStore}
|
||||
|
||||
@@ -1,469 +0,0 @@
|
||||
<script lang="ts" context="module">
|
||||
const getCurrentEditor = () => getActiveComponent('DataDuplicatorTab');
|
||||
|
||||
registerCommand({
|
||||
id: 'dataDuplicator.run',
|
||||
category: 'Data duplicator',
|
||||
name: 'Import into DB',
|
||||
keyText: 'F5 | CtrlOrCommand+Enter',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
icon: 'icon run',
|
||||
testEnabled: () => getCurrentEditor()?.canRun(),
|
||||
onClick: () => getCurrentEditor().run(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataDuplicator.kill',
|
||||
category: 'Data duplicator',
|
||||
icon: 'icon close',
|
||||
name: 'Kill',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => getCurrentEditor()?.canKill(),
|
||||
onClick: () => getCurrentEditor().kill(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataDuplicator.generateScript',
|
||||
category: 'Data duplicator',
|
||||
icon: 'img shell',
|
||||
name: 'Generate Script',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => getCurrentEditor()?.canRun(),
|
||||
onClick: () => getCurrentEditor().generateScript(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { ScriptWriter, ScriptWriterJson } from 'dbgate-tools';
|
||||
|
||||
import _ from 'lodash';
|
||||
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||
import invalidateCommands from '../commands/invalidateCommands';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import Link from '../elements/Link.svelte';
|
||||
import ObjectConfigurationControl from '../elements/ObjectConfigurationControl.svelte';
|
||||
import TableControl from '../elements/TableControl.svelte';
|
||||
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
|
||||
import CheckboxField from '../forms/CheckboxField.svelte';
|
||||
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import { extractShellConnection } from '../impexp/createImpExpScript';
|
||||
import SocketMessageView from '../query/SocketMessageView.svelte';
|
||||
import useEditorData from '../query/useEditorData';
|
||||
import { getCurrentConfig } from '../stores';
|
||||
import { apiCall, apiOff, apiOn } from '../utility/api';
|
||||
import { changeTab } from '../utility/common';
|
||||
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||
import { useArchiveFiles, useArchiveFolders, useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import useEffect from '../utility/useEffect';
|
||||
import useTimerLabel from '../utility/useTimerLabel';
|
||||
import appObjectTypes from '../appobj';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
export let tabid;
|
||||
|
||||
let busy = false;
|
||||
let runnerId = null;
|
||||
let executeNumber = 0;
|
||||
|
||||
export const activator = createActivator('DataDuplicatorTab', true);
|
||||
|
||||
const timerLabel = useTimerLabel();
|
||||
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: dbinfo = useDatabaseInfo({ conid, database });
|
||||
|
||||
$: archiveFolders = useArchiveFolders();
|
||||
$: archiveFiles = useArchiveFiles({ folder: $editorState?.value?.archiveFolder });
|
||||
|
||||
$: pairedNames = _.sortBy(
|
||||
_.intersectionBy(
|
||||
$dbinfo?.tables?.map(x => x.pureName),
|
||||
$archiveFiles?.map(x => x.name),
|
||||
(x: string) => _.toUpper(x)
|
||||
)
|
||||
);
|
||||
|
||||
$: {
|
||||
changeTab(tabid, tab => ({ ...tab, busy }));
|
||||
}
|
||||
|
||||
$: {
|
||||
busy;
|
||||
runnerId;
|
||||
tableRows;
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
const { editorState, editorValue, setEditorData } = useEditorData({
|
||||
tabid,
|
||||
onInitialData: value => {
|
||||
invalidateCommands();
|
||||
},
|
||||
});
|
||||
|
||||
function changeTable(row) {
|
||||
setEditorData(old => ({
|
||||
...old,
|
||||
tables: {
|
||||
...old?.tables,
|
||||
[row.name]: row,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function createScript(forceScript = false) {
|
||||
const config = getCurrentConfig();
|
||||
const script = config.allowShellScripting || forceScript ? new ScriptWriter() : new ScriptWriterJson();
|
||||
script.dataDuplicator({
|
||||
connection: extractShellConnection($connection, database),
|
||||
archive: $editorState.value.archiveFolder,
|
||||
items: tableRows
|
||||
.filter(x => x.isChecked)
|
||||
.map(row => ({
|
||||
name: row.name,
|
||||
operation: row.operation,
|
||||
matchColumns: _.compact([row.matchColumn1]),
|
||||
})),
|
||||
options: {
|
||||
rollbackAfterFinish: !!$editorState.value?.rollbackAfterFinish,
|
||||
skipRowsWithUnresolvedRefs: !!$editorState.value?.skipRowsWithUnresolvedRefs,
|
||||
setNullForUnresolvedNullableRefs: !!$editorState.value?.setNullForUnresolvedNullableRefs,
|
||||
},
|
||||
});
|
||||
return script.getScript();
|
||||
}
|
||||
|
||||
export function canRun() {
|
||||
return !!tableRows.find(x => x.isChecked) && !busy;
|
||||
}
|
||||
|
||||
export async function run() {
|
||||
if (busy) return;
|
||||
executeNumber += 1;
|
||||
busy = true;
|
||||
const script = await createScript();
|
||||
let runid = runnerId;
|
||||
const resp = await apiCall('runners/start', { script });
|
||||
runid = resp.runid;
|
||||
runnerId = runid;
|
||||
timerLabel.start();
|
||||
}
|
||||
|
||||
export async function generateScript() {
|
||||
const code = await createScript();
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Shell #',
|
||||
icon: 'img shell',
|
||||
tabComponent: 'ShellTab',
|
||||
},
|
||||
{ editor: code }
|
||||
);
|
||||
}
|
||||
|
||||
$: effect = useEffect(() => registerRunnerDone(runnerId));
|
||||
|
||||
function registerRunnerDone(rid) {
|
||||
if (rid) {
|
||||
apiOn(`runner-done-${rid}`, handleRunnerDone);
|
||||
return () => {
|
||||
apiOff(`runner-done-${rid}`, handleRunnerDone);
|
||||
};
|
||||
} else {
|
||||
return () => {};
|
||||
}
|
||||
}
|
||||
|
||||
$: $effect;
|
||||
|
||||
const handleRunnerDone = () => {
|
||||
busy = false;
|
||||
timerLabel.stop();
|
||||
};
|
||||
|
||||
export function canKill() {
|
||||
return busy;
|
||||
}
|
||||
|
||||
export function kill() {
|
||||
apiCall('runners/cancel', {
|
||||
runid: runnerId,
|
||||
});
|
||||
timerLabel.stop();
|
||||
}
|
||||
|
||||
// $: console.log('$archiveFiles', $archiveFiles);
|
||||
// $: console.log('$editorState', $editorState.value);
|
||||
|
||||
$: tableRows = pairedNames.map(name => {
|
||||
const item = $editorState?.value?.tables?.[name];
|
||||
const isChecked = item?.isChecked ?? true;
|
||||
const operation = item?.operation ?? 'copy';
|
||||
const tableInfo = $dbinfo?.tables?.find(x => x.pureName?.toUpperCase() == name.toUpperCase());
|
||||
const matchColumn1 =
|
||||
item?.matchColumn1 ?? tableInfo?.primaryKey?.columns?.[0]?.columnName ?? tableInfo?.columns?.[0]?.columnName;
|
||||
|
||||
return {
|
||||
name,
|
||||
isChecked,
|
||||
operation,
|
||||
matchColumn1,
|
||||
file: name,
|
||||
table: tableInfo?.schemaName ? `${tableInfo?.schemaName}.${tableInfo?.pureName}` : tableInfo?.pureName,
|
||||
schemaName: tableInfo?.schemaName,
|
||||
pureName: tableInfo?.pureName,
|
||||
tableInfo,
|
||||
};
|
||||
});
|
||||
|
||||
// $: console.log('$archiveFolders', $archiveFolders);
|
||||
|
||||
const changeCheckStatus = isChecked => () => {
|
||||
setEditorData(old => {
|
||||
const tables = { ...old?.tables };
|
||||
for (const table of pairedNames) {
|
||||
tables[table] = {
|
||||
...old?.tables?.[table],
|
||||
isChecked,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...old,
|
||||
tables,
|
||||
};
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<ToolStripContainer>
|
||||
<VerticalSplitter initialValue="70%">
|
||||
<svelte:fragment slot="1">
|
||||
<div class="wrapper">
|
||||
<ObjectConfigurationControl title="Configuration">
|
||||
<FormFieldTemplateLarge label="Source archive" type="combo">
|
||||
<SelectField
|
||||
isNative
|
||||
value={$editorState.value?.archiveFolder}
|
||||
on:change={e => {
|
||||
setEditorData(old => ({
|
||||
...old,
|
||||
archiveFolder: e.detail,
|
||||
}));
|
||||
}}
|
||||
options={$archiveFolders?.map(x => ({
|
||||
label: x.name,
|
||||
value: x.name,
|
||||
})) || []}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
|
||||
<FormFieldTemplateLarge
|
||||
label="Dry run - no changes (rollback when finished)"
|
||||
type="checkbox"
|
||||
labelProps={{
|
||||
onClick: () => {
|
||||
setEditorData(old => ({
|
||||
...old,
|
||||
rollbackAfterFinish: !$editorState.value?.rollbackAfterFinish,
|
||||
}));
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CheckboxField
|
||||
checked={$editorState.value?.rollbackAfterFinish}
|
||||
on:change={e => {
|
||||
setEditorData(old => ({
|
||||
...old,
|
||||
rollbackAfterFinish: e.target.checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
|
||||
<FormFieldTemplateLarge
|
||||
label="Skip rows with unresolved mandatory references"
|
||||
type="checkbox"
|
||||
labelProps={{
|
||||
onClick: () => {
|
||||
setEditorData(old => ({
|
||||
...old,
|
||||
skipRowsWithUnresolvedRefs: !$editorState.value?.skipRowsWithUnresolvedRefs,
|
||||
}));
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CheckboxField
|
||||
checked={$editorState.value?.skipRowsWithUnresolvedRefs}
|
||||
on:change={e => {
|
||||
setEditorData(old => ({
|
||||
...old,
|
||||
skipRowsWithUnresolvedRefs: e.target.checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
|
||||
<FormFieldTemplateLarge
|
||||
label="Set NULL for nullable unresolved references"
|
||||
type="checkbox"
|
||||
labelProps={{
|
||||
onClick: () => {
|
||||
setEditorData(old => ({
|
||||
...old,
|
||||
setNullForUnresolvedNullableRefs: !$editorState.value?.setNullForUnresolvedNullableRefs,
|
||||
}));
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CheckboxField
|
||||
checked={$editorState.value?.setNullForUnresolvedNullableRefs}
|
||||
on:change={e => {
|
||||
setEditorData(old => ({
|
||||
...old,
|
||||
setNullForUnresolvedNullableRefs: e.target.checked,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</ObjectConfigurationControl>
|
||||
|
||||
<ObjectConfigurationControl title="Imported files">
|
||||
<div class="mb-2">
|
||||
<Link onClick={changeCheckStatus(true)}>Check all</Link>
|
||||
|
|
||||
<Link onClick={changeCheckStatus(false)}>Uncheck all</Link>
|
||||
</div>
|
||||
|
||||
<TableControl
|
||||
rows={tableRows}
|
||||
columns={[
|
||||
{ header: '', fieldName: 'isChecked', slot: 1 },
|
||||
{ header: 'Source file', fieldName: 'file', slot: 4 },
|
||||
{ header: 'Target table', fieldName: 'table', slot: 5 },
|
||||
{ header: 'Operation', fieldName: 'operation', slot: 2 },
|
||||
{ header: 'Match column', fieldName: 'matchColumn1', slot: 3 },
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="1" let:row>
|
||||
<CheckboxField
|
||||
checked={row.isChecked}
|
||||
on:change={e => {
|
||||
changeTable({ ...row, isChecked: e.target.checked });
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="2" let:row>
|
||||
<SelectField
|
||||
isNative
|
||||
value={row.operation}
|
||||
on:change={e => {
|
||||
changeTable({ ...row, operation: e.detail });
|
||||
}}
|
||||
disabled={!row.isChecked}
|
||||
options={[
|
||||
{ label: 'Copy row', value: 'copy' },
|
||||
{ label: 'Lookup (find matching row)', value: 'lookup' },
|
||||
{ label: 'Insert if not exists', value: 'insertMissing' },
|
||||
]}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="3" let:row>
|
||||
{#if row.operation != 'copy'}
|
||||
<SelectField
|
||||
isNative
|
||||
value={row.matchColumn1}
|
||||
on:change={e => {
|
||||
changeTable({ ...row, matchColumn1: e.detail });
|
||||
}}
|
||||
disabled={!row.isChecked}
|
||||
options={$dbinfo?.tables
|
||||
?.find(x => x.pureName?.toUpperCase() == row.name.toUpperCase())
|
||||
?.columns?.map(col => ({
|
||||
label: col.columnName,
|
||||
value: col.columnName,
|
||||
})) || []}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="4" let:row>
|
||||
<Link
|
||||
onClick={() => {
|
||||
openNewTab({
|
||||
title: row.file,
|
||||
icon: 'img archive',
|
||||
tooltip: `${$editorState.value?.archiveFolder}\n${row.file}`,
|
||||
tabComponent: 'ArchiveFileTab',
|
||||
props: {
|
||||
archiveFile: row.file,
|
||||
archiveFolder: $editorState.value?.archiveFolder,
|
||||
},
|
||||
});
|
||||
}}><FontIcon icon="img archive" /> {row.file}</Link
|
||||
>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="5" let:row>
|
||||
<Link
|
||||
menu={appObjectTypes.DatabaseObjectAppObject.createAppObjectMenu({ ...row.tableInfo, conid, database })}
|
||||
onClick={() => {
|
||||
openNewTab({
|
||||
title: row.pureName,
|
||||
icon: 'img table',
|
||||
tabComponent: 'TableDataTab',
|
||||
props: {
|
||||
schemaName: row.schemaName,
|
||||
pureName: row.pureName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField: 'tables',
|
||||
},
|
||||
});
|
||||
}}><FontIcon icon="img table" /> {row.table}</Link
|
||||
>
|
||||
</svelte:fragment>
|
||||
</TableControl>
|
||||
</ObjectConfigurationControl>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="2">
|
||||
<SocketMessageView
|
||||
eventName={runnerId ? `runner-info-${runnerId}` : null}
|
||||
{executeNumber}
|
||||
showNoMessagesAlert
|
||||
showCaller
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</VerticalSplitter>
|
||||
|
||||
<svelte:fragment slot="toolstrip">
|
||||
<ToolStripCommandButton command="dataDuplicator.run" data-testid="DataDuplicatorTab_importIntoDb" />
|
||||
<ToolStripCommandButton command="dataDuplicator.kill" data-testid="DataDuplicatorTab_kill" />
|
||||
<ToolStripCommandButton command="dataDuplicator.generateScript" data-testid="DataDuplicatorTab_generateScript" />
|
||||
</svelte:fragment>
|
||||
</ToolStripContainer>
|
||||
|
||||
<!-- <div>
|
||||
{#each pairedNames as name}
|
||||
<div>{name}</div>
|
||||
{/each}
|
||||
</div> -->
|
||||
|
||||
<!-- <style>
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style> -->
|
||||
<style>
|
||||
.wrapper {
|
||||
overflow-y: auto;
|
||||
background-color: var(--theme-bg-0);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -2,12 +2,12 @@
|
||||
const getCurrentEditor = () => getActiveComponent('ImportExportTab');
|
||||
|
||||
registerFileCommands({
|
||||
idPrefix: 'job',
|
||||
category: 'Job',
|
||||
idPrefix: 'impexp',
|
||||
category: 'Impoirt & Export',
|
||||
getCurrentEditor,
|
||||
folder: 'jobs',
|
||||
folder: 'impexp',
|
||||
format: 'json',
|
||||
fileExtension: 'job',
|
||||
fileExtension: 'impexp',
|
||||
|
||||
// undoRedo: true,
|
||||
});
|
||||
@@ -319,7 +319,7 @@
|
||||
<ToolStripButton icon="img shell" on:click={handleGenerateScript} data-testid="ImportExportTab_generateScriptButton"
|
||||
>Generate script</ToolStripButton
|
||||
>
|
||||
<ToolStripSaveButton idPrefix="job" />
|
||||
<ToolStripSaveButton idPrefix="impexp" />
|
||||
</svelte:fragment>
|
||||
</ToolStripContainer>
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import * as ConnectionTab from './ConnectionTab.svelte';
|
||||
import * as MapTab from './MapTab.svelte';
|
||||
import * as ServerSummaryTab from './ServerSummaryTab.svelte';
|
||||
import * as ProfilerTab from './ProfilerTab.svelte';
|
||||
import * as DataDuplicatorTab from './DataDuplicatorTab.svelte';
|
||||
import * as ImportExportTab from './ImportExportTab.svelte';
|
||||
import * as SqlObjectTab from './SqlObjectTab.svelte';
|
||||
|
||||
@@ -57,7 +56,6 @@ export default {
|
||||
MapTab,
|
||||
ServerSummaryTab,
|
||||
ProfilerTab,
|
||||
DataDuplicatorTab,
|
||||
ImportExportTab,
|
||||
SqlObjectTab,
|
||||
...protabs,
|
||||
|
||||
@@ -184,7 +184,7 @@ export async function exportQuickExportFile(dataName, reader, format: QuickExpor
|
||||
|
||||
export async function saveFileToDisk(
|
||||
filePathFunc,
|
||||
options: any = { formatLabel: 'HTML page', formatExtension: 'html' }
|
||||
options: any = { formatLabel: 'HTML page', formatExtension: 'html', defaultFileName: null }
|
||||
) {
|
||||
const { formatLabel, formatExtension } = options;
|
||||
const electron = getElectron();
|
||||
@@ -193,7 +193,7 @@ export async function saveFileToDisk(
|
||||
const filters = [{ name: formatLabel, extensions: [formatExtension] }];
|
||||
const filePath = await electron.showSaveDialog({
|
||||
filters,
|
||||
defaultPath: `file.${formatExtension}`,
|
||||
defaultPath: options.defaultFileName ?? `file.${formatExtension}`,
|
||||
properties: ['showOverwriteConfirmation'],
|
||||
});
|
||||
if (!filePath) return;
|
||||
@@ -202,7 +202,7 @@ export async function saveFileToDisk(
|
||||
} else {
|
||||
const resp = await apiCall('files/generate-uploads-file');
|
||||
await filePathFunc(resp.filePath);
|
||||
await downloadFromApi(`uploads/get?file=${resp.fileName}`, `file.${formatExtension}`);
|
||||
await downloadFromApi(`uploads/get?file=${resp.fileName}`, options.defaultFileName ?? `file.${formatExtension}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function formatFileSize(size) {
|
||||
if (size > 1000000000) return `${Math.round(size / 10000000000) * 10} GB`;
|
||||
if (size > 1000000) return `${Math.round(size / 10000000) * 10} MB`;
|
||||
if (size > 1000) return `${Math.round(size / 10000) * 10} KB`;
|
||||
if (size > 1000000000) return `${Math.round(size / 100000000) / 10} GB`;
|
||||
if (size > 1000000) return `${Math.round(size / 100000) / 10} MB`;
|
||||
if (size > 1000) return `${Math.round(size / 100) / 10} KB`;
|
||||
return `${size} bytes`;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
import { apiCall } from '../utility/api';
|
||||
import { useArchiveFolders } from '../utility/metadataLoaders';
|
||||
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||
import InlineUploadButton from '../buttons/InlineUploadButton.svelte';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
|
||||
let filter = '';
|
||||
|
||||
@@ -22,11 +24,47 @@
|
||||
const handleRefreshFolders = () => {
|
||||
apiCall('archive/refresh-folders');
|
||||
};
|
||||
|
||||
async function handleUploadedFile(filePath, fileName) {
|
||||
await apiCall('archive/save-uploaded-zip', { filePath, fileName });
|
||||
}
|
||||
</script>
|
||||
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput placeholder="Search archive folders" bind:value={filter} />
|
||||
<CloseSearchButton bind:filter />
|
||||
|
||||
{#if isProApp()}
|
||||
<InlineUploadButton
|
||||
icon="icon upload"
|
||||
filters={[
|
||||
{
|
||||
name: `All supported files`,
|
||||
extensions: ['zip'],
|
||||
},
|
||||
{ name: `ZIP files`, extensions: ['zip'] },
|
||||
]}
|
||||
onProcessFile={handleUploadedFile}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- {#if electron}
|
||||
<InlineButton on:click={handleOpenElectronFile} title="Add file" data-testid="ArchiveFolderList_uploadZipFile">
|
||||
<FontIcon icon="icon plus-thick" />
|
||||
</InlineButton>
|
||||
{:else}
|
||||
<InlineButtonLabel
|
||||
on:click={() => {}}
|
||||
title="Add file"
|
||||
data-testid="ArchiveFolderList_uploadZipFile"
|
||||
htmlFor="uploadZipFileButton"
|
||||
>
|
||||
<FontIcon icon="icon plus-thick" />
|
||||
</InlineButtonLabel>
|
||||
{/if}
|
||||
|
||||
<input type="file" id="uploadZipFileButton" hidden on:change={handleUploadedFile} /> -->
|
||||
|
||||
<InlineButton on:click={() => runCommand('new.archiveFolder')} title="Add new archive folder">
|
||||
<FontIcon icon="icon plus-thick" />
|
||||
</InlineButton>
|
||||
|
||||
75
packages/web/src/widgets/MultiColumnFilterControl.svelte
Normal file
75
packages/web/src/widgets/MultiColumnFilterControl.svelte
Normal file
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
import { evalFilterBehaviour } from 'dbgate-tools';
|
||||
import DataFilterControl from '../datagrid/DataFilterControl.svelte';
|
||||
import InlineButton from '../buttons/InlineButton.svelte';
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import _ from 'lodash';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
export let compoudFilter: { [key: string]: string };
|
||||
export let onSetCompoudFilter;
|
||||
export let columnNames: string[];
|
||||
export let filterBehaviour = evalFilterBehaviour;
|
||||
|
||||
$: columnsReal = Object.keys(compoudFilter || {});
|
||||
$: columnsUsed = columnsReal.length > 0 ? columnsReal : [columnNames[0]];
|
||||
</script>
|
||||
|
||||
{#each columnsUsed as column, index}
|
||||
<div class="flex">
|
||||
<SelectField
|
||||
isNative
|
||||
value={column}
|
||||
on:change={e => {
|
||||
const keys = Object.keys(compoudFilter || {});
|
||||
const values = Object.values(compoudFilter || {});
|
||||
keys[index] = e.detail;
|
||||
const newFilter = _.zipObject(keys, values);
|
||||
onSetCompoudFilter(newFilter);
|
||||
}}
|
||||
options={columnNames.map(col => ({
|
||||
label: col,
|
||||
value: col,
|
||||
})) || []}
|
||||
/>
|
||||
|
||||
<DataFilterControl
|
||||
{filterBehaviour}
|
||||
filter={compoudFilter?.[column] ?? ''}
|
||||
setFilter={value => {
|
||||
onSetCompoudFilter({
|
||||
...compoudFilter,
|
||||
[column]: value,
|
||||
});
|
||||
}}
|
||||
placeholder="Filter"
|
||||
/>
|
||||
|
||||
{#if index == 0}
|
||||
<InlineButton
|
||||
on:click={() => {
|
||||
const newColumn = columnNames.find(x => !columnsUsed.includes(x));
|
||||
if (!newColumn) return;
|
||||
onSetCompoudFilter({
|
||||
...compoudFilter,
|
||||
[newColumn]: '',
|
||||
});
|
||||
}}
|
||||
title="Add filter column"
|
||||
square
|
||||
>
|
||||
<FontIcon icon="icon plus-thick" />
|
||||
</InlineButton>
|
||||
{:else}
|
||||
<InlineButton
|
||||
on:click={() => {
|
||||
onSetCompoudFilter(_.omit(compoudFilter, column));
|
||||
}}
|
||||
title="Remove filter column"
|
||||
square
|
||||
>
|
||||
<FontIcon icon="icon minus-thick" />
|
||||
</InlineButton>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -10,9 +10,8 @@
|
||||
import { apiCall } from '../utility/api';
|
||||
import { useFiles } from '../utility/metadataLoaders';
|
||||
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import InlineButtonLabel from '../buttons/InlineButtonLabel.svelte';
|
||||
import resolveApi, { resolveApiHeaders } from '../utility/resolveApi';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import InlineUploadButton from '../buttons/InlineUploadButton.svelte';
|
||||
|
||||
let filter = '';
|
||||
|
||||
@@ -23,12 +22,12 @@
|
||||
const queryFiles = useFiles({ folder: 'query' });
|
||||
const sqliteFiles = useFiles({ folder: 'sqlite' });
|
||||
const diagramFiles = useFiles({ folder: 'diagrams' });
|
||||
const jobFiles = useFiles({ folder: 'jobs' });
|
||||
const importExportJobFiles = useFiles({ folder: 'impexp' });
|
||||
const dataDeployJobFiles = useFiles({ folder: 'datadeploy' });
|
||||
const dbCompareJobFiles = useFiles({ folder: 'dbcompare' });
|
||||
const perspectiveFiles = useFiles({ folder: 'perspectives' });
|
||||
const modelTransformFiles = useFiles({ folder: 'modtrans' });
|
||||
|
||||
const electron = getElectron();
|
||||
|
||||
$: files = [
|
||||
...($sqlFiles || []),
|
||||
...($shellFiles || []),
|
||||
@@ -38,8 +37,10 @@
|
||||
...($sqliteFiles || []),
|
||||
...($diagramFiles || []),
|
||||
...($perspectiveFiles || []),
|
||||
...($jobFiles || []),
|
||||
...($importExportJobFiles || []),
|
||||
...($modelTransformFiles || []),
|
||||
...((isProApp() && $dataDeployJobFiles) || []),
|
||||
...((isProApp() && $dbCompareJobFiles) || []),
|
||||
];
|
||||
|
||||
function handleRefreshFiles() {
|
||||
@@ -53,50 +54,23 @@
|
||||
'sqlite',
|
||||
'diagrams',
|
||||
'perspectives',
|
||||
'jobs',
|
||||
'impexp',
|
||||
'modtrans',
|
||||
'datadeploy',
|
||||
'dbcompare',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async function handleUploadedFile(e) {
|
||||
const files = [...e.target.files];
|
||||
|
||||
for (const file of files) {
|
||||
const formData = new FormData();
|
||||
formData.append('name', file.name);
|
||||
formData.append('data', file);
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: resolveApiHeaders(),
|
||||
};
|
||||
|
||||
const apiBase = resolveApi();
|
||||
const resp = await fetch(`${apiBase}/uploads/upload-data-file`, fetchOptions);
|
||||
const fileData = await resp.json();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOpenElectronFile() {
|
||||
const filePaths = await electron.showOpenDialog({
|
||||
filters: [
|
||||
{
|
||||
name: `All supported files`,
|
||||
extensions: ['sql'],
|
||||
},
|
||||
{ name: `SQL files`, extensions: ['sql'] },
|
||||
],
|
||||
properties: ['showHiddenFiles', 'openFile'],
|
||||
});
|
||||
const filePath = filePaths && filePaths[0];
|
||||
await apiCall('uploads/save-data-file', { filePath });
|
||||
async function handleUploadedFile(filePath, fileName) {
|
||||
await apiCall('files/save-uploaded-file', { filePath, fileName });
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -104,26 +78,20 @@
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput placeholder="Search saved files" bind:value={filter} />
|
||||
<CloseSearchButton bind:filter />
|
||||
{#if electron}
|
||||
<InlineButton on:click={handleOpenElectronFile} title="Add file" data-testid="SavedFileList_buttonAddFile">
|
||||
<FontIcon icon="icon plus-thick" />
|
||||
</InlineButton>
|
||||
{:else}
|
||||
<InlineButtonLabel
|
||||
on:click={() => {}}
|
||||
title="Add file"
|
||||
data-testid="SavedFileList_buttonAddFile"
|
||||
htmlFor="uploadSavedFileButton"
|
||||
>
|
||||
<FontIcon icon="icon plus-thick" />
|
||||
</InlineButtonLabel>
|
||||
{/if}
|
||||
<InlineUploadButton
|
||||
filters={[
|
||||
{
|
||||
name: `All supported files`,
|
||||
extensions: ['sql'],
|
||||
},
|
||||
{ name: `SQL files`, extensions: ['sql'] },
|
||||
]}
|
||||
onProcessFile={handleUploadedFile}
|
||||
/>
|
||||
<InlineButton on:click={handleRefreshFiles} title="Refresh files" data-testid="SavedFileList_buttonRefresh">
|
||||
<FontIcon icon="icon refresh" />
|
||||
</InlineButton>
|
||||
</SearchBoxWrapper>
|
||||
|
||||
<input type="file" id="uploadSavedFileButton" hidden on:change={handleUploadedFile} />
|
||||
|
||||
<AppObjectList list={files} module={savedFileAppObject} groupFunc={data => dataFolderTitle(data.folder)} {filter} />
|
||||
</WidgetsInnerContainer>
|
||||
|
||||
Reference in New Issue
Block a user