censql
Version:
A NodeJS command line client for SAP HANA
565 lines (418 loc) • 16.1 kB
JavaScript
var charm = require('charm')(process.stdout);
var colors = require("colors");
var _ = require('lodash');
var pad = require('pad');
var cliTable = require('cli-table');
var stripColorCodes = require('stripcolorcodes');
var ansiSubstr = require('ansi-substring');
var async = require('async');
var StudioFormatter = function(session, screen, sqlConsole) {
this.session = session;
this.screen = screen;
this.sqlConsole = sqlConsole;
this.maxSideWidth = 60;
this.maxSqlConsoleHeight = 15;
this.metaBoxHeight = 4;
this.refreshCheckDelay = 150;
this.tableListMode = "Tables";
this.borderTheme = "bgWhite"
this.sideBackgroundTheme = "bgCyan";
this.bottomBarTheme = 'bgBlack'
this.metaBoxTheme = "bgMagenta"
this.tableBoxBackgroundTheme = {
"Tables": "bgBlue",
"Views": "bgMagenta"
};
this.scrollDataPaneDebounced = _.throttle(this.scrollDataPane.bind(this), 70, {
trailing: true
});
}
StudioFormatter.prototype.init = function(schemas, tables) {
this.schemas = schemas;
this.tables = tables;
this.dataPane = {
scroll: {
x: 0,
y: 0
}
};
this.calculateSize();
this.redraw();
setTimeout(this.checkRefresh.bind(this), this.refreshCheckDelay);
}
StudioFormatter.prototype.calculateSize = function() {
this.width = process.stdout.columns || 80;
this.height = (process.stdout.rows || 30) - 2;
this.sideWidth = (Math.ceil(this.width / 4) < this.maxSideWidth ? Math.ceil(this.width / 4) : this.maxSideWidth);
this.schemaBoxHeight = Math.ceil(this.height / 4);
this.tableBoxHeight = this.height - this.schemaBoxHeight - 5
this.sqlConsoleHeight = (Math.ceil(this.height / 5) < this.maxSqlConsoleHeight ? Math.ceil(this.height / 5) : this.maxSqlConsoleHeight);
this.dataPaneHeight = this.height - this.sqlConsoleHeight;
this.metaWindowHeight = 6;
this.sqlConsole.setRegion(
this.sideWidth + 1,
this.dataPaneHeight + 2,
this.width - this.sideWidth - 1,
this.sqlConsoleHeight
)
global.graphWidth = process.stdout.columns / 1.5;
}
StudioFormatter.prototype.checkRefresh = function() {
if (this.width != process.stdout.columns || this.height != process.stdout.rows - 2) {
/**
* Regenerate size
*/
this.calculateSize();
this.redraw();
}
setTimeout(this.checkRefresh.bind(this), this.refreshCheckDelay)
}
StudioFormatter.prototype.redraw = function() {
this.screen.clear();
this.drawBorder();
this.drawHelpText();
this.drawSchemaList()
this.drawTableList();
this.redrawDataPane();
this.sqlConsole.draw(true);
}
StudioFormatter.prototype.redrawDataPane = function() {
if (this.dataPane.mode == "tablePreview") {
this.drawTableMetaInfo();
this.drawActiveBorders();
this.drawDataView();
}
if (this.dataPane.mode == "queryOutput") {
this.clearMainPanel();
this.drawQueryHeader();
this.drawActiveBorders();
this.drawDataView();
}
this.sqlConsole.moveCursor();
}
StudioFormatter.prototype.drawQueryHeader = function(){
/**
* Draw meta box
*/
this.screen.graphics.drawBox(this.sideWidth + 1, 2, this.width - this.sideWidth - 1, this.metaBoxHeight + 1, " " [this.metaBoxTheme]);
this.screen.graphics.drawText(this.sideWidth + 2, 3, "SQL: "[this.metaBoxTheme].bold + this.dataPane.meta.query.substring(0, this.width - this.sideWidth - 8)[this.metaBoxTheme])
}
StudioFormatter.prototype.drawSchemaList = function() {
if (this.schemas.length == 0) {
return;
}
try {
var yoffset = 3;
for (var i = 0; i < Math.ceil(this.schemaBoxHeight / 2); i++) {
var name = pad(this.schemas[i % this.schemas.length].SCHEMA_NAME.substring(0, this.sideWidth - 4), this.sideWidth - 3);
if (i == 0) {
this.screen.graphics.drawText(2, yoffset + i + parseInt(this.schemaBoxHeight / 2), (" " + name)[this.sideBackgroundTheme].bold)
} else {
this.screen.graphics.drawText(2, yoffset + i + parseInt(this.schemaBoxHeight / 2), (" " + name)[this.sideBackgroundTheme])
}
}
for (var i = Math.floor(this.schemaBoxHeight / 2); i > 0; i--) {
var name = pad(this.schemas[this.schemas.length - ((i - 1) % this.schemas.length) - 1].SCHEMA_NAME.substring(0, this.sideWidth - 4), this.sideWidth - 3);
if (i == 0) {
this.screen.graphics.drawText(2, yoffset + parseInt(this.schemaBoxHeight / 2) - i, (" " + name)[this.sideBackgroundTheme].bold)
} else {
this.screen.graphics.drawText(2, yoffset + parseInt(this.schemaBoxHeight / 2) - i, (" " + name)[this.sideBackgroundTheme])
}
}
} catch (e) {
console.log(e);
}
}
StudioFormatter.prototype.drawTableList = function() {
if (this.tables.length == 0) {
this.screen.graphics.drawBox(2, this.schemaBoxHeight + 5, this.sideWidth - 2, this.tableBoxHeight + 1, " " [this.tableBoxBackgroundTheme[this.tableListMode]]);
return;
}
try {
var yoffset = 5 + this.schemaBoxHeight;
for (var i = 0; i < Math.ceil(this.tableBoxHeight / 2); i++) {
var name = pad(this.tables[i % this.tables.length].NAME.substring(0, this.sideWidth - 4), this.sideWidth - 3);
if (i == 0) {
this.screen.graphics.drawText(2, yoffset + i + parseInt(this.tableBoxHeight / 2), (" " + name)[this.tableBoxBackgroundTheme[this.tableListMode]].bold)
} else {
this.screen.graphics.drawText(2, yoffset + i + parseInt(this.tableBoxHeight / 2), (" " + name)[this.tableBoxBackgroundTheme[this.tableListMode]])
}
}
for (var i = Math.floor(this.tableBoxHeight / 2); i > 0; i--) {
var name = pad(this.tables[this.tables.length - ((i - 1) % this.tables.length) - 1].NAME.substring(0, this.sideWidth - 4), this.sideWidth - 3);
if (i == 0) {
this.screen.graphics.drawText(2, yoffset + parseInt(this.tableBoxHeight / 2) - i, (" " + name)[this.tableBoxBackgroundTheme[this.tableListMode]].bold)
} else {
this.screen.graphics.drawText(2, yoffset + parseInt(this.tableBoxHeight / 2) - i, (" " + name)[this.tableBoxBackgroundTheme[this.tableListMode]])
}
}
} catch (e) {
console.log(e);
}
}
StudioFormatter.prototype.drawActiveBorders = function() {
var focusedChar = " ".bgYellow
var nonfocusedChar = " " [this.borderTheme]
/**
* Shared by both
*/
this.screen.graphics.drawBox(this.sideWidth + 1, this.height, this.width - this.sideWidth - 1, 1, (this.session.focus == "sql-console" ? focusedChar : nonfocusedChar));
/**
* Data pane
*/
this.screen.graphics.drawBox(this.sideWidth + 1, 1, this.width - this.sideWidth - 1, 1, (this.session.focus == "data-pane" ? focusedChar : nonfocusedChar));
this.screen.graphics.drawBox(this.sideWidth, 1, 1, this.height - this.sqlConsoleHeight + 1, (this.session.focus == "data-pane" ? focusedChar : nonfocusedChar));
this.screen.graphics.drawBox(this.width, 1, 1, this.height - this.sqlConsoleHeight + 1, (this.session.focus == "data-pane" ? focusedChar : nonfocusedChar));
if (this.dataPane.mode == "tablePreview" || this.dataPane.mode == "queryOutput") {
this.screen.graphics.drawBox(this.sideWidth + 1, this.metaWindowHeight, this.width - this.sideWidth - 1, 1, (this.session.focus == "data-pane" ? focusedChar : nonfocusedChar));
}
/**
* SQL console
*/
this.screen.graphics.drawBox(this.sideWidth, this.dataPaneHeight + 1, this.width - this.sideWidth + 1, 1, focusedChar);
this.screen.graphics.drawBox(this.sideWidth, this.dataPaneHeight + 2, 1, this.sqlConsoleHeight, (this.session.focus == "sql-console" ? focusedChar : nonfocusedChar));
this.screen.graphics.drawBox(this.width, this.dataPaneHeight + 2, 1, this.sqlConsoleHeight, (this.session.focus == "sql-console" ? focusedChar : nonfocusedChar));
}
StudioFormatter.prototype.drawHelpText = function() {
/**
* Always draw this help
*/
this.screen.graphics.drawBox(1, this.height + 1, this.width, 3, " " [this.bottomBarTheme]);
this.screen.graphics.drawText(2, this.height + 1, colors.bgBlack.bold("Shft + ⮙ ⮛") + " Scroll schemas." [this.bottomBarTheme]);
this.screen.graphics.drawText(2, this.height + 2, colors.bgBlack.bold("Ctrl + ⮙ ⮛") + " Scroll tables" [this.bottomBarTheme]);
this.screen.graphics.drawText(29, this.height + 1, colors.bgBlack.bold("Shft + ⮚") + " Select schema." [this.bottomBarTheme]);
this.screen.graphics.drawText(29, this.height + 2, colors.bgBlack.bold("Ctrl + ⮚") + " Select table." [this.bottomBarTheme]);
this.screen.graphics.drawText(53, this.height + 1, colors.bgBlack.bold("Shft + Tab") + " Toggle between tables/views." [this.bottomBarTheme]);
/**
* Draw this help for datapanes
*/
if(this.session.focus == "data-pane"){
this.screen.graphics.drawText(53, this.height + 2, colors.bgBlack.bold("⮘ / ⮙ / ⮚ / ⮛") + " Scroll data pane" [this.bottomBarTheme]);
}
/**
* Draw sql console help
*/
if(this.session.focus == "sql-console"){
this.screen.graphics.drawText(53, this.height + 2, colors.bgBlack.bold("Ctrl + Enter") + " Run query" [this.bottomBarTheme]);
}
}
StudioFormatter.prototype.drawTableBoxHeader = function() {
this.screen.graphics.drawText(2, this.schemaBoxHeight + 4, colors.bgBlack(pad(" " + this.tableListMode, this.sideWidth - 2)));
}
StudioFormatter.prototype.drawBorder = function() {
/**
* Draw outer border
*/
this.screen.graphics.drawBox(1, 1, this.width, 1, " " [this.borderTheme]);
this.screen.graphics.drawBox(1, this.height, this.width, 1, " " [this.borderTheme]);
this.screen.graphics.drawBox(1, 2, 1, this.height, " " [this.borderTheme]);
this.screen.graphics.drawBox(this.width, 2, 1, this.height, " " [this.borderTheme]);
/**
* Draw side box divider
*/
this.screen.graphics.drawBox(2, this.schemaBoxHeight + 3, this.sideWidth - 2, 1, " " [this.borderTheme]);
/**
* Draw side headers
*/
this.screen.graphics.drawText(2, 2, colors.bgBlack(pad(" Schemas", this.sideWidth - 2)));
this.drawTableBoxHeader();
/**
* Draw active borders
*/
this.drawActiveBorders();
}
StudioFormatter.prototype.clearMainPanel = function() {
this.screen.graphics.drawBox(this.sideWidth + 1, 2, this.width - this.sideWidth - 1, this.dataPaneHeight, " ");
}
StudioFormatter.prototype.drawTableView = function(schema, table, columns, dataPreview, isView) {
var colNames = [];
for (var i = columns.length - 1; i >= 0; i--) {
colNames.push(columns[i].COLUMN_NAME)
}
/**
* Save this state
*/
this.dataPane.mode = "tablePreview"
this.dataPane.meta = {
schema: schema,
table: table,
columns: colNames,
isView: isView
}
this.dataPane.rawData = dataPreview;
this.dataPane.scroll = {
x: 0,
y: 0
}
var table = new cliTable({
head: this.dataPane.meta.columns
});
for (var k = 0; k < this.dataPane.rawData.length; k++) {
var rows = [];
for (var j = 0; j < this.dataPane.meta.columns.length; j++) {
var value = this.dataPane.rawData[k][this.dataPane.meta.columns[j]];
if (value == null) value = "NULL";
rows.push(value)
};
table.push(rows);
};
this.dataPane.data = table.toString().split("\n");
this.redrawDataPane();
}
StudioFormatter.prototype.drawOueryOutputView = function(query, data) {
/**
* Save this state
*/
this.dataPane.mode = "queryOutput"
this.dataPane.meta = {
query: query
}
this.dataPane.rawData = data;
this.dataPane.scroll = {
x: 0,
y: 0
}
this.screen.renderCommandOutput(query, data, function(err, lines){
this.dataPane.data = [].concat.apply([], lines);
this.redrawDataPane();
}.bind(this))
}
StudioFormatter.prototype.drawDataView = function() {
if (!this.dataPane.rawData || this.dataPane.rawData.length == 0) {
this.fullPageAlert("Nothing to show", "bgBlue", true);
return;
}
var yPadding = 7;
var x = 0;
var y = 0;
var awidth = this.width - this.sideWidth - 1;
var aheight = this.dataPaneHeight - this.metaWindowHeight;
var count = -0;
/**
* Draw table
*/
for (var i = this.dataPane.scroll.y; i < aheight + this.dataPane.scroll.y; i++) {
if (i + 1 > this.dataPane.data.length) {
break;
}
count++;
var line = this.dataPane.data[i]
line = line.replace(new RegExp("\\t", 'g'), " ").trim();
line = ansiSubstr(line, x + this.dataPane.scroll.x, x + awidth + this.dataPane.scroll.x);
line = pad(line, awidth, {
colors: true,
char: " "
});
this.screen.graphics.drawText(this.sideWidth + 1, yPadding + i - this.dataPane.scroll.y, line)
}
for (var i = count; i < aheight; i++) {
this.screen.graphics.drawText(this.sideWidth + 1, yPadding + i, pad("-", awidth, " "))
}
}
StudioFormatter.prototype.drawTableMetaInfo = function() {
this.clearMainPanel();
var xoffset = 2;
var columnString = this.dataPane.meta.columns.join(", ");
var maxColumnStringWidth = this.width - this.sideWidth - 13;
if (columnString.length > maxColumnStringWidth) {
columnString = columnString.substring(0, maxColumnStringWidth - 3) + "..."
}
/**
* Draw meta box
*/
this.screen.graphics.drawBox(this.sideWidth + 1, 2, this.width - this.sideWidth - 1, this.metaBoxHeight + 1, " " [this.metaBoxTheme]);
this.screen.graphics.drawText(this.sideWidth + xoffset, 3, "Schema Name: ".bold[this.metaBoxTheme] + this.dataPane.meta.schema.substring(0, 23)[this.metaBoxTheme])
if (this.dataPane.meta.isView) {
this.screen.graphics.drawText(this.sideWidth + xoffset + 39, 3, "View Name: ".bold[this.metaBoxTheme] + this.dataPane.meta.table.substring(0, 25)[this.metaBoxTheme])
} else {
this.screen.graphics.drawText(this.sideWidth + xoffset + 39, 3, "Table Name: ".bold[this.metaBoxTheme] + this.dataPane.meta.table.substring(0, 25)[this.metaBoxTheme])
}
this.screen.graphics.drawText(this.sideWidth + xoffset, 4, "Columns: ".bold[this.metaBoxTheme] + columnString[this.metaBoxTheme])
}
StudioFormatter.prototype.fullPageAlert = function(err, colour, shouldClear, isFullScreen) {
if (!shouldClear) {
this.clearMainPanel();
}
if (!colour) {
colour = "bgRed"
}
var s = "" + err;
var width = parseInt((this.width - this.sideWidth) * 0.50);
var stringCutdown = (s).match(new RegExp(".{1," + (width - 3) + "}", "g"));
if (stringCutdown.length == 1) {
width = stringCutdown[0].length + 3
}
var height = stringCutdown.length + 3;
var ypos = this.metaWindowHeight + (this.dataPaneHeight / 2) - (this.dataPaneHeight / (this.dataPaneHeight / height));
var xpos = parseInt(this.sideWidth + (((this.width - this.sideWidth) / 2) - (width / 2)));
if (isFullScreen) {
xpos = parseInt((this.width / 2) - (width / 2));
}
this.screen.graphics.drawBox(xpos - 1, ypos + 1, width, height, " " ["bgBlack"]);
this.screen.graphics.drawBox(xpos, ypos, width, height, " " [colour]);
for (var i = stringCutdown.length - 1; i >= 0; i--) {
this.screen.graphics.drawText(xpos + 2, ypos + 1 + i, stringCutdown[i][colour].bold);
}
}
StudioFormatter.prototype.byebye = function() {
this.screen.print("\n");
this.screen.clear()
this.screen.print(colors.green("Bye Bye!"), false)
this.screen.print("\n");
}
StudioFormatter.prototype.scrollDataPane = function(dx, dy) {
var oldx = this.dataPane.scroll.x;
var oldy = this.dataPane.scroll.y;
this.dataPane.scroll.x += dx;
this.dataPane.scroll.y += dy;
if (this.dataPane.scroll.x < 0) {
this.dataPane.scroll.x = 0;
}
if (this.dataPane.scroll.y < 0) {
this.dataPane.scroll.y = 0;
}
if (oldx != this.dataPane.scroll.x || oldy != this.dataPane.scroll.y) {
this.drawDataView();
}
}
StudioFormatter.prototype.rotateSchemas = function(d) {
if (this.schemas.length == 0) {
return;
}
for (var i = 0; i < Math.abs(d); i++) {
if (d < 0) {
this.schemas.unshift(this.schemas.pop());
}
if (d > 0) {
this.schemas.push(this.schemas.shift());
}
}
this.drawSchemaList();
}
StudioFormatter.prototype.rotateTables = function(d) {
if (this.tables.length == 0) {
return;
}
for (var i = 0; i < Math.abs(d); i++) {
if (d < 0) {
this.tables.unshift(this.tables.pop());
}
if (d > 0) {
this.tables.push(this.tables.shift());
}
}
this.drawTableList();
}
StudioFormatter.prototype.setTables = function(tables) {
this.tables = tables;
this.drawTableList();
}
StudioFormatter.prototype.setSchemas = function(schemas) {
this.schemas = schemas;
this.drawSchemaList();
}
StudioFormatter.prototype.setTableListMode = function(mode) {
this.tableListMode = mode;
this.drawTableBoxHeader();
}
module.exports = StudioFormatter;