Enabled large file downloads for desktop users within the query tool. #3369
parent
ebf4963758
commit
126e1fb53d
|
|
@ -20,6 +20,7 @@ Bundled PostgreSQL Utilities
|
|||
New features
|
||||
************
|
||||
|
||||
| `Issue #3369 <https://github.com/pgadmin-org/pgadmin4/issues/3369>`_ - Enabled large file downloads for desktop users within the query tool.
|
||||
| `Issue #8583 <https://github.com/pgadmin-org/pgadmin4/issues/8583>`_ - Add all missing options to the Import/Export Data functionality, and update the syntax of the COPY command to align with the latest standards.
|
||||
| `Issue #8681 <https://github.com/pgadmin-org/pgadmin4/issues/8681>`_ - Add support for exporting table data based on a custom query.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
//////////////////////////////////////////////////////////////
|
||||
import globals from 'globals';
|
||||
import js from '@eslint/js';
|
||||
import unusedImports from 'eslint-plugin-unused-imports';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
|
|
@ -34,6 +35,9 @@ export default [
|
|||
'platform': 'readonly',
|
||||
},
|
||||
},
|
||||
'plugins': {
|
||||
'unused-imports': unusedImports,
|
||||
},
|
||||
'rules': {
|
||||
'indent': [
|
||||
'error',
|
||||
|
|
@ -55,6 +59,17 @@ export default [
|
|||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
// We need to exclude below for RegEx case
|
||||
'no-useless-escape': 0,
|
||||
'no-unused-vars': 'off',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
'vars': 'all',
|
||||
'varsIgnorePattern': '^_',
|
||||
'args': 'after-used',
|
||||
'argsIgnorePattern': '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@
|
|||
"packageManager": "yarn@3.8.7",
|
||||
"devDependencies": {
|
||||
"electron": "36.2.0",
|
||||
"eslint": "^9.26.0"
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-unused-imports": "^4.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"electron-context-menu": "^4.0.5",
|
||||
"electron-dl": "^4.0.0",
|
||||
"electron-store": "^10.0.0"
|
||||
"electron-store": "^10.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { app, ipcMain, dialog, BrowserWindow, shell } from 'electron';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { setBadge, clearBadge, clearProgress, setProgress } from './progress.js';
|
||||
import { writeServerLog } from './misc.js';
|
||||
|
||||
class DownloadItem {
|
||||
constructor(filePath, onUpdate, onRemove) {
|
||||
this.filePath = filePath;
|
||||
this.currentLoaded = 0;
|
||||
this.total = null;
|
||||
this.stream = fs.createWriteStream(filePath);;
|
||||
this.onUpdate = onUpdate;
|
||||
this.onRemove = onRemove;
|
||||
}
|
||||
write(chunk) {
|
||||
this.stream.write(chunk);
|
||||
this.currentLoaded += chunk.length;
|
||||
this.onUpdate?.();
|
||||
}
|
||||
setTotal(total) {
|
||||
this.total = total;
|
||||
}
|
||||
remove() {
|
||||
this.stream.end();
|
||||
this.onRemove?.();
|
||||
}
|
||||
}
|
||||
|
||||
const downloadQueue = {};
|
||||
|
||||
function updateProgress(callerWindow) {
|
||||
let count = Object.keys(downloadQueue).length;
|
||||
if (count === 0) {
|
||||
clearBadge();
|
||||
clearProgress.call(callerWindow);
|
||||
return;
|
||||
}
|
||||
setBadge(Object.keys(downloadQueue).length);
|
||||
let progress = 0;
|
||||
if(Object.values(downloadQueue).some((item) => item.total === null)) {
|
||||
// If any of the items in the queue does not have a total, we cannot calculate progress
|
||||
// so we return 2 to indicate that the progress is indeterminate.
|
||||
progress = 2;
|
||||
} else {
|
||||
const total = Object.values(downloadQueue).reduce((acc, item) => {
|
||||
if (item.total) {
|
||||
return acc + item.currentLoaded / item.total;
|
||||
}
|
||||
return acc + item.currentLoaded;
|
||||
}, 0);
|
||||
progress = total / Object.keys(downloadQueue).length;
|
||||
}
|
||||
setProgress.call(callerWindow, progress);
|
||||
}
|
||||
|
||||
export function setupDownloader() {
|
||||
// Listen for the renderer's request to show the open dialog
|
||||
ipcMain.handle('get-download-path', async (event, options, prompt=true) => {
|
||||
try {
|
||||
let filePath = path.join(app.getPath('downloads'), options.defaultPath);
|
||||
const callerWindow = BrowserWindow.fromWebContents(event.sender);
|
||||
// prompt is true when the user has set the preference to prompt for download location
|
||||
if(prompt) {
|
||||
const result = await dialog.showSaveDialog(callerWindow, {
|
||||
title: 'Save File',
|
||||
...options,
|
||||
});
|
||||
|
||||
if (result.canceled) {
|
||||
return;
|
||||
}
|
||||
filePath = result.filePath;
|
||||
}
|
||||
|
||||
downloadQueue[filePath] = new DownloadItem(filePath, () => {
|
||||
updateProgress(callerWindow);
|
||||
}, () => {
|
||||
delete downloadQueue[filePath];
|
||||
updateProgress(callerWindow);
|
||||
});
|
||||
|
||||
updateProgress(callerWindow);
|
||||
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
writeServerLog(`Error in get-download-path: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('download-data-save-total', (event, filePath, total) => {
|
||||
const item = downloadQueue[filePath];
|
||||
if (item) {
|
||||
item.setTotal(total);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('download-data-save-chunk', (event, filePath, chunk) => {
|
||||
const item = downloadQueue[filePath];
|
||||
if (item) {
|
||||
item.write(chunk);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on('download-data-save-end', (event, filePath, openFile=false) => {
|
||||
const item = downloadQueue[filePath];
|
||||
if (item) {
|
||||
item.remove();
|
||||
openFile && shell.openPath(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ import { spawn } from 'child_process';
|
|||
import { fileURLToPath } from 'url';
|
||||
import { setupMenu } from './menu.js';
|
||||
import contextMenu from 'electron-context-menu';
|
||||
import { CancelError, download } from 'electron-dl';
|
||||
import { setupDownloader } from './downloader.js';
|
||||
|
||||
const configStore = new Store({
|
||||
defaults: {
|
||||
|
|
@ -153,28 +153,6 @@ function reloadApp() {
|
|||
currWin.webContents.reload();
|
||||
}
|
||||
|
||||
async function desktopFileDownload(payload) {
|
||||
const currWin = BrowserWindow.getFocusedWindow();
|
||||
try {
|
||||
await download(currWin, payload.downloadUrl, {
|
||||
filename: payload.fileName,
|
||||
saveAs: payload.prompt_for_download_location,
|
||||
onProgress: (progress) => {
|
||||
currWin.webContents.send('download-progress', progress);
|
||||
},
|
||||
onCompleted: (item) => {
|
||||
currWin.webContents.send('download-complete', item);
|
||||
if (payload.automatically_open_downloaded_file)
|
||||
shell.openPath(item.path);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (!(error instanceof CancelError)) {
|
||||
misc.writeServerLog(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This functions is used to start the pgAdmin4 server by spawning a
|
||||
// separate process.
|
||||
function startDesktopMode() {
|
||||
|
|
@ -192,8 +170,9 @@ function startDesktopMode() {
|
|||
process.env.PGADMIN_SERVER_MODE = 'OFF';
|
||||
|
||||
// Start Page URL
|
||||
startPageUrl = 'http://127.0.0.1:' + serverPort + '/?key=' + UUID;
|
||||
serverCheckUrl = 'http://127.0.0.1:' + serverPort + '/misc/ping?key=' + UUID;
|
||||
const baseUrl = `http://127.0.0.1:${serverPort}`;
|
||||
startPageUrl = `${baseUrl}/?key=${UUID}`;
|
||||
serverCheckUrl = `${baseUrl}/misc/ping?key=${UUID}`;
|
||||
|
||||
// Write Python Path, pgAdmin file path and command in log file.
|
||||
misc.writeServerLog('pgAdmin Runtime Environment');
|
||||
|
|
@ -356,6 +335,8 @@ function launchPgAdminWindow() {
|
|||
'reloadApp': reloadApp,
|
||||
});
|
||||
|
||||
setupDownloader();
|
||||
|
||||
pgAdminMainScreen.loadURL(startPageUrl);
|
||||
|
||||
const bounds = configStore.get('bounds');
|
||||
|
|
@ -429,7 +410,6 @@ ipcMain.on('log', (text) => ()=>{
|
|||
misc.writeServerLog(text);
|
||||
});
|
||||
ipcMain.on('reloadApp', reloadApp);
|
||||
ipcMain.on('onFileDownload', (_, payload) => desktopFileDownload(payload));
|
||||
ipcMain.handle('checkPortAvailable', async (_e, fixedPort)=>{
|
||||
try {
|
||||
await misc.getAvailablePort(fixedPort);
|
||||
|
|
|
|||
|
|
@ -24,5 +24,9 @@ contextBridge.exposeInMainWorld('electronUI', {
|
|||
showSaveDialog: (options) => ipcRenderer.invoke('showSaveDialog', options),
|
||||
log: (text)=> ipcRenderer.send('log', text),
|
||||
reloadApp: ()=>{ipcRenderer.send('reloadApp');},
|
||||
onFileDownload: (payload) => ipcRenderer.send('onFileDownload', payload),
|
||||
// Download related functions
|
||||
getDownloadPath: (...args) => ipcRenderer.invoke('get-download-path', ...args),
|
||||
downloadDataSaveChunk: (...args) => ipcRenderer.send('download-data-save-chunk', ...args),
|
||||
downloadDataSaveTotal: (...args) => ipcRenderer.send('download-data-save-total', ...args),
|
||||
downloadDataSaveEnd: (...args) => ipcRenderer.send('download-data-save-end', ...args),
|
||||
});
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { app } from 'electron';
|
||||
|
||||
export function setBadge(count) {
|
||||
const badgeCount = parseInt(count, 10);
|
||||
if (!isNaN(badgeCount)) {
|
||||
app.setBadgeCount(badgeCount);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to clear badge
|
||||
export function clearBadge() {
|
||||
app.setBadgeCount(0);
|
||||
}
|
||||
|
||||
// Function to set progress bar
|
||||
export function setProgress(progress) {
|
||||
const progressValue = parseFloat(progress);
|
||||
if (this && !isNaN(progressValue) && progressValue >= 0 && progressValue <= 1) {
|
||||
this.setProgressBar(progressValue);
|
||||
} else if (this && progress === -1) {
|
||||
this.setProgressBar(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to clear progress
|
||||
export function clearProgress() {
|
||||
if (this) {
|
||||
this.setProgressBar(-1);
|
||||
}
|
||||
}
|
||||
|
|
@ -751,7 +751,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron-store@npm:^10.0.0":
|
||||
"electron-store@npm:^10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "electron-store@npm:10.0.1"
|
||||
dependencies:
|
||||
|
|
@ -881,6 +881,19 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-unused-imports@npm:^4.1.4":
|
||||
version: 4.1.4
|
||||
resolution: "eslint-plugin-unused-imports@npm:4.1.4"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/eslint-plugin": ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0
|
||||
eslint: ^9.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
"@typescript-eslint/eslint-plugin":
|
||||
optional: true
|
||||
checksum: 1f4ce3e3972699345513840f3af1b783033dbc3a3e85b62ce12b3f6a89fd8c92afe46d0c00af40bacb14465445983ba0ccc326a6fd5132553061fb0e47bcba19
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-scope@npm:^8.3.0":
|
||||
version: 8.3.0
|
||||
resolution: "eslint-scope@npm:8.3.0"
|
||||
|
|
@ -1886,9 +1899,9 @@ __metadata:
|
|||
axios: ^1.9.0
|
||||
electron: 36.2.0
|
||||
electron-context-menu: ^4.0.5
|
||||
electron-dl: ^4.0.0
|
||||
electron-store: ^10.0.0
|
||||
electron-store: ^10.0.1
|
||||
eslint: ^9.26.0
|
||||
eslint-plugin-unused-imports: ^4.1.4
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import Replication from './Replication';
|
|||
import { getExpandCell } from '../../../static/js/components/PgReactTableStyled';
|
||||
import CodeMirror from '../../../static/js/components/ReactCodeMirror';
|
||||
import GetAppRoundedIcon from '@mui/icons-material/GetAppRounded';
|
||||
import { downloadFile } from '../../../static/js/utils';
|
||||
import { downloadTextData } from '../../../static/js/download_utils';
|
||||
import RefreshButton from './components/RefreshButtons';
|
||||
|
||||
function parseData(data) {
|
||||
|
|
@ -451,7 +451,7 @@ function Dashboard({
|
|||
let fileName = 'data-' + new Date().getTime() + extension;
|
||||
|
||||
try {
|
||||
downloadFile(respData, fileName, `text/${type}`);
|
||||
downloadTextData(respData, fileName, `text/${type}`);
|
||||
} catch {
|
||||
setSsMsg(gettext('Failed to download the logs.'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import Uploader from './Uploader';
|
|||
import GridView from './GridView';
|
||||
import convert from 'convert-units';
|
||||
import PropTypes from 'prop-types';
|
||||
import { downloadBlob } from '../../../../../static/js/utils';
|
||||
import { downloadBlob } from '../../../../../static/js/download_utils';
|
||||
import ErrorBoundary from '../../../../../static/js/helpers/ErrorBoundary';
|
||||
import { MY_STORAGE } from './FileManagerConstants';
|
||||
import _ from 'lodash';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
import getApiInstance from '../api_instance';
|
||||
import { downloadFile } from '../utils';
|
||||
import { downloadTextData } from '../download_utils';
|
||||
|
||||
function convertImageURLtoDataURI(api, image) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
|
@ -43,6 +43,6 @@ export function downloadSvg(svg, svgName) {
|
|||
}
|
||||
|
||||
Promise.all(image_promises).then(function() {
|
||||
downloadFile(svgElement.outerHTML, svgName, 'image/svg+xml');
|
||||
downloadTextData(svgElement.outerHTML, svgName, 'image/svg+xml');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
//////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
import usePreferences from '../../preferences/static/js/store';
|
||||
import { callFetch, parseApiError } from './api_instance';
|
||||
import { getBrowser, toPrettySize } from './utils';
|
||||
|
||||
// This function is used to download the base64 data
|
||||
// and create a link to download the file.
|
||||
export async function downloadBase64UrlData(downloadUrl, fileName) {
|
||||
let link = document.createElement('a');
|
||||
link.setAttribute('href', downloadUrl);
|
||||
link.setAttribute('download', fileName);
|
||||
link.style.setProperty('visibility ', 'hidden');
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
// This function is used to download the blob data
|
||||
export async function downloadBlob(blob, fileName) {
|
||||
const urlCreator = window.URL || window.webkitURL;
|
||||
const downloadUrl = urlCreator.createObjectURL(blob);
|
||||
|
||||
downloadBase64UrlData(downloadUrl, fileName);
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
}
|
||||
|
||||
// This function is used to download the text data
|
||||
export function downloadTextData(textData, fileName, fileType) {
|
||||
const respBlob = new Blob([textData], {type : fileType});
|
||||
downloadBlob(respBlob, fileName);
|
||||
}
|
||||
|
||||
// This function is used to download the file from the given URL
|
||||
// and use streaming to download the file in chunks where there
|
||||
// is no limit on the file size.
|
||||
export async function downloadFileStream(allOptions, fileName, fileType, onProgress) {
|
||||
const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
|
||||
|
||||
const start = async (filePath, writer) => {
|
||||
const response = await callFetch(allOptions.url, allOptions.options);
|
||||
if(!response.ok) {
|
||||
throw new Error(parseApiError(await response.json()));
|
||||
}
|
||||
if (!response.body) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
const contentLength = response.headers.get('Content-Length');
|
||||
writer.downloadDataSaveTotal(filePath, contentLength ? parseInt(contentLength, 10) : null);
|
||||
|
||||
const reader = response.body.getReader();
|
||||
|
||||
let done = false;
|
||||
let receivedLength = 0; // received bytes
|
||||
while (!done) {
|
||||
const { value, done: doneReading } = await reader.read();
|
||||
done = doneReading;
|
||||
if (value) {
|
||||
writer.downloadDataSaveChunk(filePath, value);
|
||||
receivedLength += value.length;
|
||||
onProgress?.(toPrettySize(receivedLength, 'B', 2));
|
||||
}
|
||||
}
|
||||
writer.downloadDataSaveEnd(filePath, automatically_open_downloaded_file);
|
||||
};
|
||||
|
||||
let writer;
|
||||
let filePath = '';
|
||||
try {
|
||||
if(getBrowser().name != 'Electron') {
|
||||
// In other browsers, we use the blob to download the file.
|
||||
const data = [];
|
||||
writer = {
|
||||
downloadDataSaveChunk: (_fp, chunk) => {
|
||||
data.push(chunk);
|
||||
},
|
||||
downloadDataSaveTotal: () => {
|
||||
// This is not used in the browser
|
||||
},
|
||||
downloadDataSaveEnd: () => {
|
||||
// This is not used in the browser
|
||||
},
|
||||
};
|
||||
await start(filePath, writer);
|
||||
const blob = new Blob(data, {type: fileType});
|
||||
downloadBlob(blob, fileName);
|
||||
} else {
|
||||
writer = window.electronUI;
|
||||
filePath = await window.electronUI.getDownloadPath({
|
||||
defaultPath: fileName,
|
||||
}, prompt_for_download_location);
|
||||
|
||||
// If the user cancels the download, we will not proceed
|
||||
if(!filePath) {
|
||||
return;
|
||||
}
|
||||
await start(filePath, writer);
|
||||
}
|
||||
} catch (error) {
|
||||
writer.downloadDataSaveEnd(filePath);
|
||||
throw new Error('Download failed: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
|
@ -369,46 +369,7 @@ export function checkTrojanSource(content, isPasteEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function downloadBlob(blob, fileName) {
|
||||
const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
|
||||
const urlCreator = window.URL || window.webkitURL;
|
||||
const downloadUrl = urlCreator.createObjectURL(blob);
|
||||
if (getBrowser().name == 'IE' && window.navigator.msSaveBlob) {
|
||||
// IE10+ : (has Blob, but not a[download] or URL)
|
||||
window.navigator.msSaveBlob(blob, fileName);
|
||||
} else if (getBrowser().name == 'Electron') {
|
||||
await window.electronUI.onFileDownload({downloadUrl, fileName, automatically_open_downloaded_file, prompt_for_download_location});
|
||||
} else {
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', downloadUrl);
|
||||
link.setAttribute('download', fileName);
|
||||
link.style.setProperty('visibility ', 'hidden');
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadUrlData(downloadUrl, fileName) {
|
||||
const {automatically_open_downloaded_file, prompt_for_download_location} = usePreferences.getState().getPreferencesForModule('misc');
|
||||
if (getBrowser().name == 'Electron') {
|
||||
window.electronUI.onFileDownload({downloadUrl, fileName, automatically_open_downloaded_file, prompt_for_download_location});
|
||||
} else {
|
||||
let link = document.createElement('a');
|
||||
link.setAttribute('href', downloadUrl);
|
||||
link.setAttribute('download', fileName);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadFile(textData, fileName, fileType) {
|
||||
const respBlob = new Blob([textData], {type : fileType});
|
||||
downloadBlob(respBlob, fileName);
|
||||
}
|
||||
|
||||
export function toPrettySize(rawSize, from='B') {
|
||||
export function toPrettySize(rawSize, from='B', decimalFixed=null) {
|
||||
try {
|
||||
//if the integer need to be converted to K for thousands, M for millions , B for billions only
|
||||
if (from == '') {
|
||||
|
|
@ -416,6 +377,9 @@ export function toPrettySize(rawSize, from='B') {
|
|||
}
|
||||
let conVal = convert(rawSize).from(from).toBest();
|
||||
conVal.val = Math.round(conVal.val * 100) / 100;
|
||||
if(decimalFixed) {
|
||||
conVal.val = conVal.val.toFixed(decimalFixed);
|
||||
}
|
||||
return `${conVal.val} ${conVal.unit}`;
|
||||
}
|
||||
catch {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import pgAdmin from 'sources/pgadmin';
|
|||
import { styled } from '@mui/material/styles';
|
||||
import BeforeUnload from './BeforeUnload';
|
||||
import { isMac } from '../../../../../../static/js/keyboard_shortcuts';
|
||||
import { downloadUrlData } from '../../../../../../static/js/utils';
|
||||
import { downloadBase64UrlData } from '../../../../../../static/js/download_utils';
|
||||
|
||||
/* Custom react-diagram action for keyboard events */
|
||||
export class KeyboardShortcutAction extends Action {
|
||||
|
|
@ -601,7 +601,7 @@ export default class ERDTool extends React.Component {
|
|||
this.closeOnSave = closeOnSave;
|
||||
if(this.state.current_file && !isSaveAs) {
|
||||
this.saveFile(this.state.current_file);
|
||||
} else if (this.diagram.getNodesData().length > 0){ {
|
||||
} else if (this.diagram.getNodesData().length > 0){
|
||||
let params = {
|
||||
'supported_types': ['*','pgerd'],
|
||||
'dialog_type': 'create_file',
|
||||
|
|
@ -610,7 +610,6 @@ export default class ERDTool extends React.Component {
|
|||
};
|
||||
this.props.pgAdmin.Tools.FileManager.show(params, this.saveFile.bind(this), null, this.context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveFile(fileName) {
|
||||
|
|
@ -761,7 +760,7 @@ export default class ERDTool extends React.Component {
|
|||
}
|
||||
toPng(this.canvasEle, {width, height})
|
||||
.then((dataUrl)=>{
|
||||
downloadUrlData(dataUrl, `${this.getCurrentProjectName()}.png`);
|
||||
downloadBase64UrlData(dataUrl, `${this.getCurrentProjectName()}.png`);
|
||||
}).catch((err)=>{
|
||||
console.error(err);
|
||||
let msg = gettext('Unknown error. Check console logs');
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ import { LineChart, BarChart, PieChart, DATA_POINT_STYLE, DATA_POINT_SIZE,
|
|||
LightenDarkenColor} from 'sources/chartjs';
|
||||
import { QueryToolEventsContext, QueryToolContext } from '../QueryToolComponent';
|
||||
import { QUERY_TOOL_EVENTS, PANELS } from '../QueryToolConstants';
|
||||
import { downloadUrlData, getChartColor } from '../../../../../../static/js/utils';
|
||||
import { downloadBase64UrlData } from '../../../../../../static/js/download_utils';
|
||||
import { getChartColor } from '../../../../../../static/js/utils';
|
||||
|
||||
const StyledBox = styled(Box)(({theme}) => ({
|
||||
width: '100%',
|
||||
|
|
@ -380,7 +381,7 @@ export function GraphVisualiser({initColumns}) {
|
|||
const onDownloadGraph = async ()=> {
|
||||
let downloadUrl = chartObjRef.current.toBase64Image(),
|
||||
fileName = 'graph_visualiser-' + new Date().getTime() + '.png';
|
||||
downloadUrlData(downloadUrl, fileName);
|
||||
downloadBase64UrlData(downloadUrl, fileName);
|
||||
};
|
||||
|
||||
// This plugin is used to set the background color of the canvas. Very useful
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import _ from 'lodash';
|
|||
import { styled } from '@mui/material/styles';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import QueryToolDataGrid, { GRID_ROW_SELECT_KEY } from '../QueryToolDataGrid';
|
||||
import {CONNECTION_STATUS, PANELS, QUERY_TOOL_EVENTS} from '../QueryToolConstants';
|
||||
import {CONNECTION_STATUS, PANELS, QUERY_TOOL_EVENTS, MODAL_DIALOGS} from '../QueryToolConstants';
|
||||
import url_for from 'sources/url_for';
|
||||
import getApiInstance, { parseApiError } from '../../../../../../static/js/api_instance';
|
||||
import { QueryToolContext, QueryToolEventsContext } from '../QueryToolComponent';
|
||||
|
|
@ -22,7 +22,7 @@ import { LayoutDockerContext } from '../../../../../../static/js/helpers/Layout'
|
|||
import { GeometryViewer } from './GeometryViewer';
|
||||
import Explain from '../../../../../../static/js/Explain';
|
||||
import { QuerySources } from './QueryHistory';
|
||||
import { downloadFile } from '../../../../../../static/js/utils';
|
||||
import { downloadFileStream } from '../../../../../../static/js/download_utils';
|
||||
import CopyData from '../QueryToolDataGrid/CopyData';
|
||||
import moment from 'moment';
|
||||
import ConfirmSaveContent from '../../../../../../static/js/Dialogs/ConfirmSaveContent';
|
||||
|
|
@ -31,7 +31,6 @@ import { GraphVisualiser } from './GraphVisualiser';
|
|||
import { usePgAdmin } from '../../../../../../static/js/PgAdminProvider';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import ConnectServerContent from '../../../../../../static/js/Dialogs/ConnectServerContent';
|
||||
import { MODAL_DIALOGS } from '../QueryToolConstants';
|
||||
|
||||
const StyledBox = styled(Box)(({theme}) => ({
|
||||
display: 'flex',
|
||||
|
|
@ -514,23 +513,17 @@ export class ResultSetUtils {
|
|||
});
|
||||
}
|
||||
|
||||
async saveResultsToFile(fileName) {
|
||||
async saveResultsToFile(fileName, onProgress) {
|
||||
try {
|
||||
let {data: respData} = await this.api.post(
|
||||
url_for('sqleditor.query_tool_download', {
|
||||
this.hasQueryCommitted = false;
|
||||
await downloadFileStream({
|
||||
url: url_for('sqleditor.query_tool_download', {
|
||||
'trans_id': this.transId,
|
||||
}),
|
||||
{filename: fileName, query_commited: this.hasQueryCommitted}
|
||||
);
|
||||
|
||||
if(!_.isUndefined(respData.data)) {
|
||||
if(!respData.status) {
|
||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.SET_MESSAGE, respData.data.result);
|
||||
}
|
||||
} else {
|
||||
this.hasQueryCommitted = false;
|
||||
downloadFile(respData, fileName, 'text/csv');
|
||||
}
|
||||
options: {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({filename: fileName, query_commited: this.hasQueryCommitted})
|
||||
}}, fileName, 'text/csv', onProgress);
|
||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_RESULTS_END);
|
||||
} catch (error) {
|
||||
this.eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_SAVE_RESULTS_END);
|
||||
|
|
@ -1049,7 +1042,9 @@ export function ResultSet() {
|
|||
fileName = queryToolCtx.params.node_name + extension;
|
||||
}
|
||||
setLoaderText(gettext('Downloading results...'));
|
||||
await rsu.current.saveResultsToFile(fileName);
|
||||
await rsu.current.saveResultsToFile(fileName, (p)=>{
|
||||
setLoaderText(gettext('Downloading results(%s)...', p));
|
||||
});
|
||||
setLoaderText('');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import FileManager, { FileManagerUtils, getComparator } from '../../../pgadmin/m
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from 'axios';
|
||||
import getApiInstance from '../../../pgadmin/static/js/api_instance';
|
||||
import * as pgUtils from '../../../pgadmin/static/js/utils';
|
||||
import * as downloadUtils from '../../../pgadmin/static/js/download_utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const files = [
|
||||
|
|
@ -116,8 +116,8 @@ describe('FileManger', ()=>{
|
|||
let closeModal=jest.fn(),
|
||||
onOK=jest.fn(),
|
||||
onCancel=jest.fn(),
|
||||
ctrlMount = async (props)=>{
|
||||
return await render(<Theme>
|
||||
ctrlMount = (props)=>{
|
||||
return render(<Theme>
|
||||
<FileManager
|
||||
params={params}
|
||||
closeModal={closeModal}
|
||||
|
|
@ -135,7 +135,7 @@ describe('FileManger', ()=>{
|
|||
networkMock.onPost(`/file_manager/save_last_dir/${transId}`).reply(200, {'success':1,'errormsg':'','info':'','result':null,'data':null});
|
||||
let ctrl;
|
||||
await act(async ()=>{
|
||||
ctrl = await ctrlMount({});
|
||||
ctrl = ctrlMount({});
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
await user.click(ctrl.container.querySelector('[name="menu-options"]'));
|
||||
|
|
@ -157,7 +157,7 @@ describe('FileManger', ()=>{
|
|||
let ctrl;
|
||||
const user = userEvent.setup();
|
||||
await act(async ()=>{
|
||||
ctrl = await ctrlMount({});
|
||||
ctrl = ctrlMount({});
|
||||
});
|
||||
|
||||
await user.click(ctrl.container.querySelector('[name="menu-shared-storage"]'));
|
||||
|
|
@ -171,7 +171,7 @@ describe('FileManger', ()=>{
|
|||
let ctrl;
|
||||
const user = userEvent.setup();
|
||||
await act(async ()=>{
|
||||
ctrl = await ctrlMount({});
|
||||
ctrl = ctrlMount({});
|
||||
});
|
||||
|
||||
await user.click(ctrl.container.querySelector('[name="menu-shared-storage"]'));
|
||||
|
|
@ -345,9 +345,9 @@ describe('FileManagerUtils', ()=>{
|
|||
});
|
||||
|
||||
it('downloadFile', async ()=>{
|
||||
jest.spyOn(pgUtils, 'downloadBlob').mockImplementation(() => {});
|
||||
jest.spyOn(downloadUtils, 'downloadBlob').mockImplementation(() => {});
|
||||
let row = {Filename: 'newfile1', Path: '/home/newfile1', 'storage_folder': 'my_storage'};
|
||||
await fmObj.downloadFile(row);
|
||||
expect(pgUtils.downloadBlob).toHaveBeenCalledWith('blobdata', 'newfile1');
|
||||
expect(downloadUtils.downloadBlob).toHaveBeenCalledWith('blobdata', 'newfile1');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue