mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
2) Fixed an issue where empty binary path throwing an error instead of proper error message.
530 lines
18 KiB
JavaScript
530 lines
18 KiB
JavaScript
/////////////////////////////////////////////////////////////
|
|
//
|
|
// pgAdmin 4 - PostgreSQL Tools
|
|
//
|
|
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
|
|
// This software is released under the PostgreSQL Licence
|
|
//
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
import React, { useEffect, useMemo } from 'react';
|
|
import gettext from 'sources/gettext';
|
|
import url_for from 'sources/url_for';
|
|
import _ from 'lodash';
|
|
import pgWindow from 'sources/window';
|
|
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
|
import current_user from 'pgadmin.user_management.current_user';
|
|
import VariableSchema from '../../../../browser/server_groups/servers/static/js/variable.ui';
|
|
import { getConnectionParameters } from '../../../../browser/server_groups/servers/static/js/server.ui';
|
|
import { flattenSelectOptions } from '../../../../static/js/components/FormComponents';
|
|
import ConnectServerContent from '../../../../static/js/Dialogs/ConnectServerContent';
|
|
import SchemaView from '../../../../static/js/SchemaView';
|
|
import PropTypes from 'prop-types';
|
|
import getApiInstance from '../../../../static/js/api_instance';
|
|
import { useModal } from '../../../../static/js/helpers/ModalProvider';
|
|
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
|
|
import * as commonUtils from 'sources/utils';
|
|
import * as showQueryTool from '../../../../tools/sqleditor/static/js/show_query_tool';
|
|
import { getTitle, generateTitle } from '../../../../tools/sqleditor/static/js/sqleditor_title';
|
|
import usePreferences from '../../../../preferences/static/js/store';
|
|
import { BROWSER_PANELS, WORKSPACES } from '../../../../browser/static/js/constants';
|
|
import { isEmptyString } from '../../../../static/js/validators';
|
|
import { getRandomInt } from '../../../../static/js/utils';
|
|
import { useWorkspace } from './WorkspaceProvider';
|
|
|
|
class AdHocConnectionSchema extends BaseUISchema {
|
|
constructor(connectExistingServer, initValues={}) {
|
|
super({
|
|
sid: null,
|
|
did: null,
|
|
user: null,
|
|
server_name: null,
|
|
database_name: null,
|
|
connected: false,
|
|
host: '',
|
|
port: undefined,
|
|
username: current_user.name,
|
|
role: null,
|
|
password: undefined,
|
|
service: undefined,
|
|
connection_params: [
|
|
{'name': 'sslmode', 'value': 'prefer', 'keyword': 'sslmode'},
|
|
{'name': 'connect_timeout', 'value': 10, 'keyword': 'connect_timeout'}],
|
|
...initValues,
|
|
connection_refresh: 0,
|
|
});
|
|
this.flatServers = [];
|
|
this.groupedServers = [];
|
|
this.dbs = [];
|
|
this.api = getApiInstance();
|
|
this.connectExistingServer = connectExistingServer;
|
|
this.paramSchema = new VariableSchema(getConnectionParameters, null, null, ['name', 'keyword', 'value']);
|
|
}
|
|
|
|
refreshServerList() {
|
|
// its better to refresh the server list than monkey patching server connected status.
|
|
this.groupedServers = [];
|
|
this.state.setUnpreparedData(['connection_refresh'], getRandomInt(1, 9999));
|
|
}
|
|
|
|
setServerConnected(sid, icon) {
|
|
for(const group of this.groupedServers) {
|
|
for(const opt of group.options) {
|
|
if(opt.value == sid) {
|
|
opt.connected = true;
|
|
opt.image = icon || 'icon-pg';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
isServerConnected(sid) {
|
|
return _.find(this.flatServers, (s) => s.value == sid)?.connected;
|
|
}
|
|
|
|
getServerList() {
|
|
if(this.groupedServers?.length != 0) {
|
|
return Promise.resolve(this.groupedServers);
|
|
}
|
|
return new Promise((resolve, reject)=>{
|
|
this.api.get(url_for('sqleditor.get_new_connection_servers'))
|
|
.then(({data: respData})=>{
|
|
let groupedOptions = [];
|
|
_.forIn(respData.data.result.server_list, (v, k)=>{
|
|
if(v.length == 0) {
|
|
return;
|
|
}
|
|
groupedOptions.push({
|
|
label: k,
|
|
options: v,
|
|
});
|
|
});
|
|
/* Will be re-used for changing icon when connected */
|
|
this.groupedServers = groupedOptions.map((group)=>{
|
|
return {
|
|
label: group.label,
|
|
options: group.options.map((o)=>({...o, selected: false})),
|
|
};
|
|
});
|
|
resolve(groupedOptions);
|
|
})
|
|
.catch((error)=>{
|
|
reject(error instanceof Error ? error : Error(gettext('Something went wrong')));
|
|
});
|
|
});
|
|
}
|
|
|
|
getOtherOptions(sid, type) {
|
|
if(!sid) {
|
|
return [];
|
|
}
|
|
|
|
if(!this.isServerConnected(sid)) {
|
|
return [];
|
|
}
|
|
return new Promise((resolve, reject)=>{
|
|
this.api.get(url_for(`sqleditor.${type}`, {
|
|
'sid': sid,
|
|
'sgid': 0,
|
|
}))
|
|
.then(({data: respData})=>{
|
|
resolve(respData.data.result.data);
|
|
})
|
|
.catch((error)=>{
|
|
reject(error instanceof Error ? error : Error(gettext('Something went wrong')));
|
|
});
|
|
});
|
|
}
|
|
|
|
get baseFields() {
|
|
let self = this;
|
|
return [
|
|
{
|
|
id: 'sid', label: gettext('Existing Server (Optional)'), deps: ['connected', 'connection_refresh'],
|
|
type: (state) => ({
|
|
type: 'select',
|
|
options: () => self.getServerList(),
|
|
optionsLoaded: (res) => self.flatServers = flattenSelectOptions(res),
|
|
optionsReloadBasis: `${self.flatServers.map((s) => s.connected).join('')}${state.connection_refresh}`,
|
|
}),
|
|
depChange: (state, source)=>{
|
|
// Check for connection status
|
|
let selectedServer = _.find(
|
|
self.flatServers, (s) => s.value == state.sid
|
|
);
|
|
if(source.includes('connection_refresh')) {
|
|
return {
|
|
connected: selectedServer?.connected
|
|
};
|
|
}
|
|
return {
|
|
server_name: null,
|
|
did: null,
|
|
user: null,
|
|
role: null,
|
|
sid: null,
|
|
host: null,
|
|
port: null,
|
|
service: null,
|
|
connection_params: null,
|
|
password: null,
|
|
connected: selectedServer?.connected
|
|
};
|
|
},
|
|
deferredDepChange: (state, source, topState, actionObj) => {
|
|
if(source.includes('connection_refresh')) return;
|
|
return new Promise((resolve) => {
|
|
let sid = actionObj.value;
|
|
let selectedServer = _.find(self.flatServers, (s)=>s.value==sid);
|
|
if(sid && !_.find(self.flatServers, (s) => s.value == sid)?.connected) {
|
|
this.connectExistingServer(sid, state.user, null, (data) => {
|
|
self.setServerConnected(sid, data.icon);
|
|
resolve(() => ({ sid: sid, server_name:selectedServer?.label, host: selectedServer?.host,
|
|
port: selectedServer?.port, service: selectedServer?.service,
|
|
connection_params: selectedServer?.connection_params, connected: true
|
|
}));
|
|
});
|
|
} else {
|
|
resolve(()=>({ sid: sid, server_name:selectedServer?.label, host: selectedServer?.host,
|
|
port: selectedServer?.port, service: selectedServer?.service,
|
|
connection_params: selectedServer?.connection_params, connected: true
|
|
}));
|
|
}
|
|
});
|
|
},
|
|
},
|
|
{
|
|
id: 'server_name', label: gettext('Server Name'), type: 'text', noEmpty: true,
|
|
deps: ['sid', 'connected'],
|
|
disabled: (state) => state.sid,
|
|
}, {
|
|
id: 'host', label: gettext('Host name/address'), type: 'text',
|
|
deps: ['sid', 'connected'],
|
|
disabled: (state) => state.sid,
|
|
}, {
|
|
id: 'port', label: gettext('Port'), type: 'int', min: 1, max: 65535,
|
|
deps: ['sid', 'connected'],
|
|
disabled: (state) => state.sid,
|
|
},{
|
|
id: 'did', label: gettext('Database'), deps: ['sid', 'connected'],
|
|
controlProps: {creatable: true},
|
|
type: (state) => {
|
|
if (state?.sid) {
|
|
return {
|
|
type: 'select',
|
|
options: () => this.getOtherOptions(
|
|
state.sid, 'get_new_connection_database'
|
|
),
|
|
optionsReloadBasis: `${state.sid} ${this.isServerConnected(state.sid)}`,
|
|
};
|
|
} else {
|
|
return {type: 'text'};
|
|
}
|
|
},
|
|
optionsLoaded: (res) => this.dbs = res,
|
|
depChange: (state) => {
|
|
/* Once the option is selected get the name */
|
|
return {
|
|
database_name: _.find(this.dbs, (s) => s.value == state.did)?.label
|
|
};
|
|
}
|
|
}, {
|
|
id: 'user', label: gettext('User'), deps: ['sid', 'connected'],
|
|
controlProps: {creatable: true},
|
|
type: (state) => {
|
|
if (state?.sid) {
|
|
return {
|
|
type: 'select',
|
|
options: () => this.getOtherOptions(
|
|
state.sid, 'get_new_connection_user'
|
|
),
|
|
optionsReloadBasis: `${state.sid} ${this.isServerConnected(state.sid)}`,
|
|
};
|
|
} else {
|
|
return {type: 'text'};
|
|
}
|
|
},
|
|
}, {
|
|
id: 'password', label: gettext('Password'), type: 'password',
|
|
controlProps: {
|
|
maxLength: null,
|
|
autoComplete: 'new-password'
|
|
},
|
|
deps: ['sid', 'did', 'user', 'role'],
|
|
depChange: (state, source)=> {
|
|
if (source == 'sid' || source == 'did' || source == 'user' || source == 'role') {
|
|
state.password = null;
|
|
}
|
|
}
|
|
},{
|
|
id: 'role', label: gettext('Role'), deps: ['sid', 'connected'],
|
|
controlProps: {creatable: true},
|
|
type: (state)=>({
|
|
type: 'select',
|
|
options: () => this.getOtherOptions(
|
|
state.sid, 'get_new_connection_role'
|
|
),
|
|
optionsReloadBasis: `${state.sid} ${this.isServerConnected(state.sid)}`,
|
|
}),
|
|
},{
|
|
id: 'service', label: gettext('Service'), type: 'text', deps: ['sid', 'connected'],
|
|
disabled: (state) => state.sid,
|
|
}, {
|
|
id: 'connection_params', label: gettext('Connection Parameters'),
|
|
type: 'collection',
|
|
schema: this.paramSchema, mode: ['edit', 'create'], uniqueCol: ['name'],
|
|
canAdd: true, canEdit: false, canDelete: true,
|
|
}, {
|
|
id: 'connected', label: '', type: 'text', visible: false,
|
|
}, {
|
|
id: 'database_name', label: '', type: 'text', visible: false,
|
|
}
|
|
];
|
|
}
|
|
|
|
validate(state, setError) {
|
|
let errmsg = null;
|
|
|
|
if (isEmptyString(state.service)) {
|
|
errmsg = gettext('Either Host name or Service must be specified.');
|
|
if(isEmptyString(state.host)) {
|
|
setError('host', errmsg);
|
|
return true;
|
|
} else {
|
|
setError('host', null);
|
|
}
|
|
|
|
/* Hostname, IP address validate */
|
|
if (state.host) {
|
|
// Check for leading and trailing spaces.
|
|
if (/(^\s)|(\s$)/.test(state.host)){
|
|
errmsg = gettext('Host name must be valid hostname or IPv4 or IPv6 address.');
|
|
setError('host', errmsg);
|
|
return true;
|
|
} else {
|
|
setError('host', null);
|
|
}
|
|
}
|
|
if(isEmptyString(state.port)) {
|
|
errmsg = gettext('Port must be specified.');
|
|
setError('port', errmsg);
|
|
return true;
|
|
} else {
|
|
setError('port', null);
|
|
}
|
|
|
|
if(isEmptyString(state.did)) {
|
|
errmsg = gettext('Database must be specified.');
|
|
setError('did', errmsg);
|
|
return true;
|
|
} else {
|
|
setError('did', null);
|
|
}
|
|
|
|
if(isEmptyString(state.user)) {
|
|
errmsg = gettext('User must be specified.');
|
|
setError('user', errmsg);
|
|
return true;
|
|
} else {
|
|
setError('user', null);
|
|
}
|
|
} else {
|
|
_.each(['host', 'port', 'did', 'user'], (item) => {
|
|
setError(item, null);
|
|
});
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export default function AdHocConnection({mode}) {
|
|
const api = getApiInstance();
|
|
const modal = useModal();
|
|
const pgAdmin = usePgAdmin();
|
|
const preferencesStore = usePreferences();
|
|
const {currentWorkspace} = useWorkspace();
|
|
|
|
const connectExistingServer = async (sid, user, formData, connectCallback) => {
|
|
try {
|
|
let {data: respData} = await api({
|
|
method: 'POST',
|
|
url: url_for('sqleditor.connect_server', {
|
|
'sid': sid,
|
|
...(user ? {
|
|
'usr': user,
|
|
}:{}),
|
|
}),
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
data: formData
|
|
});
|
|
connectCallback?.(respData.data);
|
|
} catch (error) {
|
|
if(!error.response) {
|
|
pgAdmin.Browser.notifier.pgNotifier('error', error, 'Connection error', gettext('Connection to pgAdmin server has been lost.'));
|
|
} else {
|
|
modal.showModal(gettext('Connect to server'), (closeModal)=>{
|
|
return (
|
|
<ConnectServerContent
|
|
closeModal={()=>{
|
|
closeModal();
|
|
}}
|
|
data={error.response?.data?.result}
|
|
onOK={(formData)=>{
|
|
connectExistingServer(sid, null, formData, connectCallback);
|
|
}}
|
|
hideSavePassword={true}
|
|
/>
|
|
);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const openQueryTool = (respData, formData)=>{
|
|
const transId = commonUtils.getRandomInt(1, 9999999);
|
|
let db_name = _.isNil(formData.database_name) ? formData.did : formData.database_name;
|
|
let user_name = formData.role || formData.user || respData.data.user.name;
|
|
|
|
let parentData = {
|
|
server_group: {_id: 1},
|
|
server: {
|
|
_id: respData.data.sid,
|
|
server_type: respData.data.server_type,
|
|
},
|
|
database: {
|
|
_id: respData.data.did,
|
|
label: db_name,
|
|
_label: db_name,
|
|
},
|
|
};
|
|
|
|
const gridUrl = showQueryTool.generateUrl(transId, parentData, null);
|
|
const title = getTitle(pgAdmin, preferencesStore.getPreferencesForModule('browser'), null, false, formData.server_name, db_name, user_name);
|
|
showQueryTool.launchQueryTool(pgWindow.pgAdmin.Tools.SQLEditor, transId, gridUrl, title, {
|
|
user: formData.user,
|
|
role: formData.role,
|
|
});
|
|
};
|
|
|
|
const openPSQLTool = (respData, formData)=> {
|
|
const transId = commonUtils.getRandomInt(1, 9999999);
|
|
let db_name = _.isNil(formData.database_name) ? formData.did : formData.database_name;
|
|
let user_name = formData.role || formData.user || respData.data.user.name;
|
|
|
|
let panelTitle = '';
|
|
// Set psql tab title as per prefrences setting.
|
|
let title_data = {
|
|
'database': db_name ? _.unescape(db_name) : 'postgres' ,
|
|
'username': user_name,
|
|
'server': formData.server_name,
|
|
'type': 'psql_tool',
|
|
};
|
|
let tab_title_placeholder = usePreferences.getState().getPreferencesForModule('browser').psql_tab_title_placeholder;
|
|
panelTitle = generateTitle(tab_title_placeholder, title_data);
|
|
|
|
let openUrl = url_for('psql.panel', {
|
|
trans_id: transId,
|
|
});
|
|
const misc_preferences = usePreferences.getState().getPreferencesForModule('misc');
|
|
let theme = misc_preferences.theme;
|
|
|
|
openUrl += `?sgid=${1}`
|
|
+`&sid=${respData.data.sid}`
|
|
+`&did=${respData.data.did}`
|
|
+`&server_type=${respData.data.server_type}`
|
|
+ `&theme=${theme}`;
|
|
|
|
if(formData.did) {
|
|
openUrl += `&db=${encodeURIComponent(db_name)}`;
|
|
} else {
|
|
openUrl += `&db=${''}`;
|
|
}
|
|
|
|
const escapedTitle = _.escape(panelTitle);
|
|
const open_new_tab = usePreferences.getState().getPreferencesForModule('browser').new_browser_tab_open;
|
|
|
|
pgAdmin.Browser.Events.trigger(
|
|
'pgadmin:tool:show',
|
|
`${BROWSER_PANELS.PSQL_TOOL}_${transId}`,
|
|
openUrl,
|
|
{title: escapedTitle, db: db_name},
|
|
{title: panelTitle, icon: 'pg-font-icon icon-terminal', manualClose: false, renamable: true},
|
|
Boolean(open_new_tab?.includes('psql_tool'))
|
|
);
|
|
|
|
return true;
|
|
};
|
|
|
|
const onSaveClick = async (isNew, formData) => {
|
|
try {
|
|
let {data: respData} = await api({
|
|
method: 'POST',
|
|
url: url_for('workspace.adhoc_connect_server'),
|
|
data: JSON.stringify(formData)
|
|
});
|
|
if (mode == WORKSPACES.QUERY_TOOL) {
|
|
openQueryTool(respData, formData);
|
|
} else if (mode == WORKSPACES.PSQL_TOOL && pgAdmin['enable_psql']) {
|
|
openPSQLTool(respData, formData);
|
|
}
|
|
} catch (error) {
|
|
if(!error.response) {
|
|
pgAdmin.Browser.notifier.pgNotifier('error', error, 'Connection error', gettext('Connect to server.'));
|
|
} else {
|
|
formData['sid'] = error.response?.data?.result?.sid;
|
|
modal.showModal(gettext('Connect to server'), (closeModal)=>{
|
|
return (
|
|
<ConnectServerContent
|
|
closeModal={()=>{
|
|
closeModal();
|
|
}}
|
|
data={error.response?.data?.result}
|
|
onOK={(okFormData)=>{
|
|
formData['password'] = okFormData.get('password');
|
|
formData['tunnel_password'] = okFormData.get('tunnel_password');
|
|
onSaveClick(isNew, formData);
|
|
}}
|
|
hideSavePassword={true}
|
|
/>
|
|
);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
let saveBtnName = gettext('Connect & Open Query Tool');
|
|
if (mode == WORKSPACES.PSQL_TOOL) {
|
|
saveBtnName = gettext('Connect & Open PSQL');
|
|
}
|
|
|
|
let adHocConObj = useMemo(() => new AdHocConnectionSchema(connectExistingServer), []);
|
|
|
|
useEffect(()=>{
|
|
if(currentWorkspace == mode) adHocConObj.refreshServerList();
|
|
}, [currentWorkspace]);
|
|
|
|
return <SchemaView
|
|
formType={'dialog'}
|
|
getInitData={() => { /*This is intentional (SonarQube)*/ }}
|
|
formClassName={'AdHocConnection-container'}
|
|
schema={adHocConObj}
|
|
viewHelperProps={{
|
|
mode: 'create',
|
|
}}
|
|
loadingText={'Connecting...'}
|
|
onSave={onSaveClick}
|
|
customSaveBtnName= {saveBtnName}
|
|
customCloseBtnName={''}
|
|
customSaveBtnIconType={mode}
|
|
hasSQL={false}
|
|
disableSqlHelp={true}
|
|
disableDialogHelp={true}
|
|
isTabView={false}
|
|
/>;
|
|
}
|
|
|
|
AdHocConnection.propTypes = {
|
|
mode: PropTypes.string
|
|
};
|