503 lines
15 KiB
JavaScript
503 lines
15 KiB
JavaScript
/////////////////////////////////////////////////////////////
|
|
//
|
|
// pgAdmin 4 - PostgreSQL Tools
|
|
//
|
|
// Copyright (C) 2013 - 2026, The pgAdmin Development Team
|
|
// This software is released under the PostgreSQL Licence
|
|
//
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
import AIReport from './AIReport';
|
|
import { AllPermissionTypes, BROWSER_PANELS } from '../../../browser/static/js/constants';
|
|
import getApiInstance from '../../../static/js/api_instance';
|
|
import MainMenuFactory from '../../../browser/static/js/MainMenuFactory';
|
|
import url_for from 'sources/url_for';
|
|
|
|
// AI Reports Module
|
|
define([
|
|
'sources/gettext', 'pgadmin.browser',
|
|
], function(
|
|
gettext, pgBrowser
|
|
) {
|
|
|
|
// if module is already initialized, refer to that.
|
|
if (pgBrowser.AITools) {
|
|
return pgBrowser.AITools;
|
|
}
|
|
|
|
// Create an Object AITools of pgBrowser class
|
|
pgBrowser.AITools = {
|
|
llmEnabled: false,
|
|
llmSystemEnabled: false,
|
|
llmStatusChecked: false,
|
|
|
|
init: function() {
|
|
if (this.initialized)
|
|
return;
|
|
|
|
this.initialized = true;
|
|
|
|
// Check LLM status and only register menus if enabled
|
|
this.checkLLMStatus();
|
|
|
|
return this;
|
|
},
|
|
|
|
// Register AI Reports menus
|
|
registerMenus: function() {
|
|
// Register AI Reports menu category
|
|
pgBrowser.add_menu_category({
|
|
name: 'ai_tools',
|
|
label: gettext('AI Reports'),
|
|
priority: 100,
|
|
});
|
|
|
|
// Define the menus
|
|
let menus = [];
|
|
|
|
// =====================================================================
|
|
// Security Reports - Server, Database, Schema
|
|
// =====================================================================
|
|
menus.push({
|
|
name: 'ai_security_report',
|
|
module: this,
|
|
applies: ['tools'],
|
|
callback: 'show_security_report',
|
|
category: 'ai_tools',
|
|
priority: 1,
|
|
label: gettext('Security'),
|
|
icon: 'fa fa-shield-alt',
|
|
enable: this.security_report_enabled.bind(this),
|
|
data: {
|
|
data_disabled: gettext('Please select a server, database, or schema.'),
|
|
},
|
|
permission: AllPermissionTypes.TOOLS_AI,
|
|
});
|
|
|
|
// Context menus for security reports
|
|
for (let node_val of ['server', 'database', 'schema']) {
|
|
menus.push({
|
|
name: 'ai_security_report_context_' + node_val,
|
|
node: node_val,
|
|
module: this,
|
|
applies: ['context'],
|
|
callback: 'show_security_report',
|
|
category: 'ai_tools',
|
|
priority: 100,
|
|
label: gettext('Security'),
|
|
icon: 'fa fa-shield-alt',
|
|
enable: this.security_report_enabled.bind(this),
|
|
permission: AllPermissionTypes.TOOLS_AI,
|
|
});
|
|
}
|
|
|
|
// =====================================================================
|
|
// Performance Reports - Server, Database
|
|
// =====================================================================
|
|
menus.push({
|
|
name: 'ai_performance_report',
|
|
module: this,
|
|
applies: ['tools'],
|
|
callback: 'show_performance_report',
|
|
category: 'ai_tools',
|
|
priority: 2,
|
|
label: gettext('Performance'),
|
|
icon: 'fa fa-tachometer-alt',
|
|
enable: this.performance_report_enabled.bind(this),
|
|
data: {
|
|
data_disabled: gettext('Please select a server or database.'),
|
|
},
|
|
permission: AllPermissionTypes.TOOLS_AI,
|
|
});
|
|
|
|
// Context menus for performance reports (server and database only)
|
|
for (let node_val of ['server', 'database']) {
|
|
menus.push({
|
|
name: 'ai_performance_report_context_' + node_val,
|
|
node: node_val,
|
|
module: this,
|
|
applies: ['context'],
|
|
callback: 'show_performance_report',
|
|
category: 'ai_tools',
|
|
priority: 101,
|
|
label: gettext('Performance'),
|
|
icon: 'fa fa-tachometer-alt',
|
|
enable: this.performance_report_enabled.bind(this),
|
|
permission: AllPermissionTypes.TOOLS_AI,
|
|
});
|
|
}
|
|
|
|
// =====================================================================
|
|
// Design Review Reports - Database, Schema
|
|
// =====================================================================
|
|
menus.push({
|
|
name: 'ai_design_report',
|
|
module: this,
|
|
applies: ['tools'],
|
|
callback: 'show_design_report',
|
|
category: 'ai_tools',
|
|
priority: 3,
|
|
label: gettext('Design'),
|
|
icon: 'fa fa-drafting-compass',
|
|
enable: this.design_report_enabled.bind(this),
|
|
data: {
|
|
data_disabled: gettext('Please select a database or schema.'),
|
|
},
|
|
permission: AllPermissionTypes.TOOLS_AI,
|
|
});
|
|
|
|
// Context menus for design review (database and schema only)
|
|
for (let node_val of ['database', 'schema']) {
|
|
menus.push({
|
|
name: 'ai_design_report_context_' + node_val,
|
|
node: node_val,
|
|
module: this,
|
|
applies: ['context'],
|
|
callback: 'show_design_report',
|
|
category: 'ai_tools',
|
|
priority: 102,
|
|
label: gettext('Design'),
|
|
icon: 'fa fa-drafting-compass',
|
|
enable: this.design_report_enabled.bind(this),
|
|
permission: AllPermissionTypes.TOOLS_AI,
|
|
});
|
|
}
|
|
|
|
pgBrowser.add_menus(menus);
|
|
},
|
|
|
|
// Check if LLM is configured, register menus only if system-enabled
|
|
checkLLMStatus: function() {
|
|
const api = getApiInstance();
|
|
api.get(url_for('llm.status'))
|
|
.then((res) => {
|
|
if (res?.data?.success) {
|
|
this.llmEnabled = res.data.data?.enabled || false;
|
|
this.llmSystemEnabled = res.data.data?.system_enabled || false;
|
|
}
|
|
this.llmStatusChecked = true;
|
|
|
|
// Only register menus if LLM is enabled at system level
|
|
if (this.llmSystemEnabled) {
|
|
this.registerMenus();
|
|
MainMenuFactory.createMainMenus();
|
|
}
|
|
})
|
|
.catch(() => {
|
|
this.llmEnabled = false;
|
|
this.llmSystemEnabled = false;
|
|
this.llmStatusChecked = true;
|
|
});
|
|
},
|
|
|
|
// Get the node type from tree item
|
|
getNodeType: function(item) {
|
|
let tree = pgBrowser.tree;
|
|
let nodeData = tree.itemData(item);
|
|
|
|
if (!nodeData) return null;
|
|
return nodeData._type;
|
|
},
|
|
|
|
// Common LLM enablement check
|
|
checkLLMEnabled: function(data) {
|
|
if (!this.llmStatusChecked) {
|
|
if (data) {
|
|
data.data_disabled = gettext(
|
|
'Checking AI configuration...'
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!this.llmSystemEnabled) {
|
|
if (data) {
|
|
data.data_disabled = gettext(
|
|
'AI features are disabled in the '
|
|
+ 'server configuration.'
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!this.llmEnabled) {
|
|
if (data) {
|
|
data.data_disabled = gettext(
|
|
'Please configure an LLM provider in '
|
|
+ 'Preferences > AI to enable this feature.'
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
// =====================================================================
|
|
// Security Report Functions
|
|
// =====================================================================
|
|
|
|
security_report_enabled: function(node, item, data) {
|
|
if (!this.checkLLMEnabled(data)) return false;
|
|
|
|
if (!node || !item) return false;
|
|
|
|
let tree = pgBrowser.tree;
|
|
let info = tree.getTreeNodeHierarchy(item);
|
|
|
|
if (!info || !info.server) {
|
|
if (data) {
|
|
data.data_disabled = gettext('Please select a server, database, or schema.');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!info.server.connected) {
|
|
if (data) {
|
|
data.data_disabled = gettext('Please connect to the server first.');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
let nodeType = this.getNodeType(item);
|
|
if (!['server', 'database', 'schema'].includes(nodeType)) {
|
|
if (data) {
|
|
data.data_disabled = gettext('Please select a server, database, or schema.');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (nodeType === 'database' || nodeType === 'schema') {
|
|
if (!info.database || !info.database.connected) {
|
|
if (data) {
|
|
data.data_disabled = gettext('Please connect to the database first.');
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
show_security_report: function() {
|
|
this._showReport('security', ['server', 'database', 'schema']);
|
|
},
|
|
|
|
// =====================================================================
|
|
// Performance Report Functions
|
|
// =====================================================================
|
|
|
|
performance_report_enabled: function(node, item, data) {
|
|
if (!this.checkLLMEnabled(data)) return false;
|
|
|
|
if (!node || !item) return false;
|
|
|
|
let tree = pgBrowser.tree;
|
|
let info = tree.getTreeNodeHierarchy(item);
|
|
|
|
if (!info || !info.server) {
|
|
if (data) {
|
|
data.data_disabled = gettext('Please select a server or database.');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!info.server.connected) {
|
|
if (data) {
|
|
data.data_disabled = gettext('Please connect to the server first.');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
let nodeType = this.getNodeType(item);
|
|
if (!['server', 'database'].includes(nodeType)) {
|
|
if (data) {
|
|
data.data_disabled = gettext('Please select a server or database.');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (nodeType === 'database') {
|
|
if (!info.database || !info.database.connected) {
|
|
if (data) {
|
|
data.data_disabled = gettext('Please connect to the database first.');
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
show_performance_report: function() {
|
|
this._showReport('performance', ['server', 'database']);
|
|
},
|
|
|
|
// =====================================================================
|
|
// Design Review Functions
|
|
// =====================================================================
|
|
|
|
design_report_enabled: function(node, item, data) {
|
|
if (!this.checkLLMEnabled(data)) return false;
|
|
|
|
if (!node || !item) return false;
|
|
|
|
let tree = pgBrowser.tree;
|
|
let info = tree.getTreeNodeHierarchy(item);
|
|
|
|
if (!info || !info.server) {
|
|
if (data) {
|
|
data.data_disabled = gettext('Please select a database or schema.');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!info.server.connected) {
|
|
if (data) {
|
|
data.data_disabled = gettext('Please connect to the server first.');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
let nodeType = this.getNodeType(item);
|
|
if (!['database', 'schema'].includes(nodeType)) {
|
|
if (data) {
|
|
data.data_disabled = gettext('Please select a database or schema.');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!info.database || !info.database.connected) {
|
|
if (data) {
|
|
data.data_disabled = gettext('Please connect to the database first.');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
show_design_report: function() {
|
|
this._showReport('design', ['database', 'schema']);
|
|
},
|
|
|
|
// =====================================================================
|
|
// Common Report Display Function
|
|
// =====================================================================
|
|
|
|
_showReport: function(reportCategory, validNodeTypes) {
|
|
let t = pgBrowser.tree,
|
|
i = t.selected(),
|
|
info = pgBrowser.tree.getTreeNodeHierarchy(i);
|
|
|
|
if (!info || !info.server) {
|
|
pgBrowser.report_error(
|
|
gettext('Report'),
|
|
gettext('Please select a valid node.')
|
|
);
|
|
return;
|
|
}
|
|
|
|
let nodeType = this.getNodeType(i);
|
|
if (!validNodeTypes.includes(nodeType)) {
|
|
pgBrowser.report_error(
|
|
gettext('Report'),
|
|
gettext('Please select a valid node for this report type.')
|
|
);
|
|
return;
|
|
}
|
|
|
|
let sid = info.server._id;
|
|
let did = info.database ? info.database._id : null;
|
|
let scid = info.schema ? info.schema._id : null;
|
|
|
|
// Determine report type based on node
|
|
let reportType = nodeType;
|
|
|
|
// Build panel title and ID with timestamp for uniqueness
|
|
let panelTitle = this._buildPanelTitle(reportCategory, reportType, info);
|
|
let panelIdSuffix = this._buildPanelIdSuffix(reportCategory, reportType, sid, did, scid);
|
|
const timestamp = Date.now();
|
|
const panelId = `${BROWSER_PANELS.AI_REPORT_PREFIX}-${panelIdSuffix}-${timestamp}`;
|
|
|
|
// Get docker handler and open as tab in main panel area
|
|
let handler = pgBrowser.getDockerHandler?.(
|
|
BROWSER_PANELS.AI_REPORT_PREFIX,
|
|
pgBrowser.docker.default_workspace
|
|
);
|
|
if (!handler) {
|
|
pgBrowser.report_error(
|
|
gettext('Report'),
|
|
gettext('Unable to open the report panel.')
|
|
);
|
|
return;
|
|
}
|
|
handler.focus();
|
|
handler.docker.openTab({
|
|
id: panelId,
|
|
title: panelTitle,
|
|
icon: 'fa fa-file-alt',
|
|
content: (
|
|
<AIReport
|
|
sid={sid}
|
|
did={did}
|
|
scid={scid}
|
|
reportCategory={reportCategory}
|
|
reportType={reportType}
|
|
serverName={info.server.label}
|
|
databaseName={info.database?.label}
|
|
schemaName={info.schema?.label}
|
|
onClose={() => { handler.docker.close(panelId); }}
|
|
/>
|
|
),
|
|
closable: true,
|
|
cache: false,
|
|
group: 'playground'
|
|
}, BROWSER_PANELS.MAIN, 'middle', true);
|
|
},
|
|
|
|
_buildPanelTitle: function(reportCategory, reportType, info) {
|
|
let categoryLabel;
|
|
switch (reportCategory) {
|
|
case 'security':
|
|
categoryLabel = gettext('Security Report');
|
|
break;
|
|
case 'performance':
|
|
categoryLabel = gettext('Performance Report');
|
|
break;
|
|
case 'design':
|
|
categoryLabel = gettext('Design Review');
|
|
break;
|
|
default:
|
|
categoryLabel = gettext('Report');
|
|
}
|
|
|
|
if (reportType === 'server') {
|
|
return info.server.label + ' ' + categoryLabel;
|
|
} else if (reportType === 'database') {
|
|
return info.database.label + ' ' + gettext('on') + ' ' +
|
|
info.server.label + ' ' + categoryLabel;
|
|
} else if (reportType === 'schema') {
|
|
return info.schema.label + ' ' + gettext('in') + ' ' +
|
|
info.database.label + ' ' + gettext('on') + ' ' +
|
|
info.server.label + ' ' + categoryLabel;
|
|
}
|
|
return categoryLabel;
|
|
},
|
|
|
|
_buildPanelIdSuffix: function(reportCategory, reportType, sid, did, scid) {
|
|
let base = `${reportCategory}_${reportType}`;
|
|
if (reportType === 'server') {
|
|
return `${base}_${sid}`;
|
|
} else if (reportType === 'database') {
|
|
return `${base}_${sid}_${did}`;
|
|
} else if (reportType === 'schema') {
|
|
return `${base}_${sid}_${did}_${scid}`;
|
|
}
|
|
return base;
|
|
},
|
|
};
|
|
|
|
return pgBrowser.AITools;
|
|
});
|