Merged with upstream

This commit is contained in:
Rohith Menon 2017-09-26 23:13:02 -07:00
commit 04ddb289ee
20 changed files with 224 additions and 59 deletions

View File

@ -1781,6 +1781,9 @@ fieldset[disabled] .form-control {
textarea.form-control {
height: auto;
}
textarea.v-resizable {
resize: vertical;
}
input[type="search"] {
-webkit-appearance: none;
}
@ -8592,6 +8595,7 @@ a.thumbnail.active {
padding: 0;
white-space: nowrap;
margin: 0;
margin-bottom: 10px;
overflow: auto;
box-shadow: inset 0 -2px 0 0 #eee;
}

View File

@ -42,7 +42,7 @@
<td>{{row.started_at | toMilliseconds | dateTime }}</td>
<td>{{row.updated_at | toMilliseconds | dateTime }}</td>
<td ng-if="selectedTab === 'in_progress'">
<cancel-query-button query-id="dataRow.query_id" task-id="dataRow.task_id"></cancel-query-button>
<cancel-query-button query-id="row.query_id" task-id="row.task_id"></cancel-query-button>
</td>
</tr>
</tbody>

View File

@ -17,6 +17,8 @@ const AddWidgetDialog = {
this.query = {};
this.selected_query = undefined;
this.text = '';
this.existing_text = '';
this.new_text = '';
this.widgetSizes = [{
name: 'Regular',
value: 1,

View File

@ -4,11 +4,11 @@
</div>
<div class="modal-body">
<div class="form-group">
<textarea class="form-control" ng-model="$ctrl.widget.text" rows="3"></textarea>
<textarea class="form-control" ng-model="$ctrl.widget.new_text" rows="3"></textarea>
</div>
<div ng-show="$ctrl.widget.text">
<div ng-show="$ctrl.widget.new_text">
<strong>Preview:</strong>
<p ng-bind-html="$ctrl.widget.text | markdown"></p>
<p ng-bind-html="$ctrl.widget.new_text | markdown"></p>
</div>
</div>

View File

@ -15,6 +15,8 @@ const EditTextBoxComponent = {
this.widget = this.resolve.widget;
this.saveWidget = () => {
this.saveInProgress = true;
if (this.widget.new_text !== this.widget.existing_text) {
this.widget.text = this.widget.new_text;
this.widget.$save().then(() => {
this.close();
}).catch(() => {
@ -22,6 +24,9 @@ const EditTextBoxComponent = {
}).finally(() => {
this.saveInProgress = false;
});
} else {
this.close();
}
};
},
};
@ -30,6 +35,8 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
this.canViewQuery = currentUser.hasPermission('view_query');
this.editTextBox = () => {
this.widget.existing_text = this.widget.text;
this.widget.new_text = this.widget.text;
$uibModal.open({
component: 'editTextBox',
resolve: {

View File

@ -1,10 +1,13 @@
import debug from 'debug';
import moment from 'moment';
import { uniq, contains, values, some, each, isArray, isNumber, isString } from 'underscore';
import { uniq, contains, values, some, each, isArray, isNumber, isString, includes } from 'underscore';
const logger = debug('redash:services:QueryResult');
const filterTypes = ['filter', 'multi-filter', 'multiFilter'];
const ALL_VALUES = '*';
const NONE_VALUES = '-';
function getColumnNameWithoutType(column) {
let typeSplit;
if (column.indexOf('::') !== -1) {
@ -208,6 +211,21 @@ function QueryResultService($resource, $timeout, $q) {
this.filterFreeze = filterFreeze;
if (filters) {
filters.forEach((filter) => {
if (filter.multiple && includes(filter.current, ALL_VALUES)) {
filter.current = filter.values.slice(1);
}
if (filter.current.length === (filter.values.length - 1)) {
filter.values[0] = NONE_VALUES;
}
if (filter.multiple && includes(filter.current, NONE_VALUES)) {
filter.current = [];
filter.values[0] = ALL_VALUES;
}
});
this.filteredData = this.query_result.data.rows.filter(row =>
filters.reduce((memo, filter) => {
if (!isArray(filter.current)) {
@ -378,6 +396,12 @@ function QueryResultService($resource, $timeout, $q) {
});
});
filters.forEach((filter) => {
if (filter.multiple) {
filter.values.unshift(ALL_VALUES);
}
});
filters.forEach((filter) => {
filter.values = uniq(filter.values, (v) => {
if (moment.isMoment(v)) {

View File

@ -134,7 +134,7 @@
<div class="form-group" ng-if="options.globalSeriesType == 'custom'">
<label class="control-label">Custom code</label>
<textarea ng-model="options.customCode" class="form-control" rows="10">
<textarea ng-model="options.customCode" class="form-control v-resizable" rows="10">
</textarea>
</div>
@ -145,6 +145,12 @@
</label>
</div>
<div class="checkbox" ng-if="options.globalSeriesType == 'custom'">
<label>
<input type="checkbox" ng-model="options.autoRedraw">
<i class="input-helper"></i> Auto update graph
</label>
</div>
<div ng-show="currentTab == 'xAxis'">
<div class="form-group">

View File

@ -87,10 +87,12 @@ function ChartEditor(ColorPalette, clientConfig) {
scope.showSizeColumnPicker = () => some(scope.options.seriesOptions, options => options.type === 'bubble');
if (scope.options.customCode === undefined) {
scope.options.customCode = `// Available variables are x, ys, element, and Plotly
// Type console.log(x, ys); for more info about x and ys
// To plot your graph call Plotly.plot(element, ...)
// Plotly examples and docs: https://plot.ly/javascript/`;
}
function refreshColumns() {
scope.columns = scope.queryResult.getColumns();

View File

@ -507,6 +507,9 @@ const CustomPlotlyChart = (clientConfig) => {
return;
}
const refresh = () => {
// Clear existing data with blank data for succeeding codeCall adds data to existing plot.
Plotly.newPlot(element[0].children[0]);
// eslint-disable-next-line no-eval
const codeCall = eval(`codeCall = function(x, ys, element, Plotly){ ${scope.options.customCode} }`);
codeCall(scope.x, scope.ys, element[0].children[0], Plotly);
@ -522,9 +525,11 @@ const CustomPlotlyChart = (clientConfig) => {
});
});
};
scope.$watch('options.customCode', () => {
scope.$watch('[options.customCode, options.autoRedraw]', () => {
try {
if (scope.options.autoRedraw) {
refresh();
}
} catch (err) {
if (scope.options.enableConsoleLogs) {
// eslint-disable-next-line no-console

View File

@ -1,4 +1,13 @@
<div class="form-horizontal">
<ul class="tab-nav">
<li ng-class="{active: currentTab == 'general'}">
<a ng-click="changeTab('general')">General</a>
</li>
<li ng-class="{active: currentTab == 'format'}">
<a ng-click="changeTab('format')">Format</a>
</li>
</ul>
<div ng-show="currentTab == 'general'">
<div class="form-group">
<label class="col-lg-6">Counter Value Column Name</label>
<div class="col-lg-6">
@ -31,4 +40,38 @@
<i class="input-helper"></i> Count Rows
</div>
</div>
</div>
<div ng-show="currentTab == 'format'">
<div class="form-group">
<label class="col-lg-6">Formatting Decimal Place</label>
<div class="col-lg-6">
<input type="number" ng-model="visualization.options.stringDecimal" class="form-control" ng-disabled="!isValueNumber()">
</div>
</div>
<div class="form-group">
<label class="col-lg-6">Formatting Decimal Character</label>
<div class="col-lg-6">
<input type="text" ng-model="visualization.options.stringDecChar" class="form-control" ng-disabled="!isValueNumber()">
</div>
</div>
<div class="form-group">
<label class="col-lg-6">Formatting Thousands Separator</label>
<div class="col-lg-6">
<input type="text" ng-model="visualization.options.stringThouSep" class="form-control" ng-disabled="!isValueNumber()" ng-trim="false">
</div>
</div>
<div class="form-group">
<label class="col-lg-6">Formatting String Prefix</label>
<div class="col-lg-6">
<input type="text" ng-model="visualization.options.stringPrefix" class="form-control" ng-disabled="!isValueNumber()" ng-trim="false">
</div>
</div>
<div class="form-group">
<label class="col-lg-6">Formatting String Suffix</label>
<div class="col-lg-6">
<input type="text" ng-model="visualization.options.stringSuffix" class="form-control" ng-disabled="!isValueNumber()" ng-trim="false">
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,6 @@
<counter ng-class="{'positive': targetValue && trendPositive, 'negative': targetValue && !trendPositive}">
<value>{{counterValue|number}}</value>
<value ng-if="isNumber">{{stringPrefix}}{{counterValue|number}}{{stringSuffix}}</value>
<value ng-if="!isNumber">{{stringPrefix}}{{counterValue}}{{stringSuffix}}</value>
<counter-target ng-if="targetValue">({{targetValue|number}})</counter-target>
<counter-name>{{visualization.name}}</counter-name>
</counter>

View File

@ -1,3 +1,6 @@
import numberFormat from 'underscore.string/numberFormat';
import { isNumber as isNum } from 'underscore';
import counterTemplate from './counter.html';
import counterEditorTemplate from './counter-editor.html';
@ -42,6 +45,26 @@ function CounterRenderer() {
} else {
$scope.targetValue = null;
}
$scope.isNumber = isNum($scope.counterValue);
if ($scope.isNumber) {
$scope.stringPrefix = $scope.visualization.options.stringPrefix;
$scope.stringSuffix = $scope.visualization.options.stringSuffix;
const stringDecimal = $scope.visualization.options.stringDecimal;
const stringDecChar = $scope.visualization.options.stringDecChar;
const stringThouSep = $scope.visualization.options.stringThouSep;
if (stringDecimal || stringDecChar || stringThouSep) {
$scope.counterValue = numberFormat($scope.counterValue,
stringDecimal,
stringDecChar,
stringThouSep);
$scope.isNumber = false;
}
} else {
$scope.stringPrefix = null;
$scope.stringSuffix = null;
}
}
};
@ -55,6 +78,26 @@ function CounterEditor() {
return {
restrict: 'E',
template: counterEditorTemplate,
link(scope) {
scope.currentTab = 'general';
scope.changeTab = (tab) => {
scope.currentTab = tab;
};
scope.isValueNumber = () => {
const queryData = scope.queryResult.getData();
if (queryData) {
const rowNumber = getRowNumber(scope.visualization.options.rowNumber, queryData.length);
const counterColName = scope.visualization.options.counterColName;
if (scope.visualization.options.countRow) {
scope.counterValue = queryData.length;
} else if (counterColName) {
scope.counterValue = queryData[rowNumber][counterColName];
}
}
return isNum(scope.counterValue);
};
},
};
}
@ -74,6 +117,9 @@ export default function (ngModule) {
counterColName: 'counter',
rowNumber: 1,
targetRowNumber: 1,
stringDecimal: 0,
stringDecChar: '.',
stringThouSep: ',',
};
VisualizationProvider.registerVisualization({

View File

@ -10,6 +10,7 @@ logger = logging.getLogger(__name__)
try:
from cassandra.cluster import Cluster
from cassandra.auth import PlainTextAuthProvider
from cassandra.util import sortedset
enabled = True
except ImportError:
enabled = False
@ -19,6 +20,8 @@ class CassandraJSONEncoder(JSONEncoder):
def default(self, o):
if isinstance(o, uuid.UUID):
return str(o)
if isinstance(o, sortedset):
return list(o)
return super(CassandraJSONEncoder, self).default(o)
@ -68,7 +71,23 @@ class Cassandra(BaseQueryRunner):
def get_schema(self, get_stats=False):
query = """
SELECT columnfamily_name, column_name FROM system.schema_columns where keyspace_name ='{}';
select release_version from system.local;
"""
results, error = self.run_query(query, None)
results = json.loads(results)
release_version = results['rows'][0]['release_version']
query = """
SELECT table_name, column_name
FROM system_schema.columns
WHERE keyspace_name ='{}';
""".format(self.configuration['keyspace'])
if release_version.startswith('2'):
query = """
SELECT columnfamily_name AS table_name, column_name
FROM system.schema_columns
WHERE keyspace_name ='{}';
""".format(self.configuration['keyspace'])
results, error = self.run_query(query, None)
@ -76,7 +95,7 @@ class Cassandra(BaseQueryRunner):
schema = {}
for row in results['rows']:
table_name = row['columnfamily_name']
table_name = row['table_name']
column_name = row['column_name']
if table_name not in schema:
schema[table_name] = {'name': table_name, 'columns': []}

View File

@ -137,13 +137,17 @@ class Mysql(BaseSQLQueryRunner):
db=self.configuration['db'],
port=self.configuration.get('port', 3306),
charset='utf8', use_unicode=True,
ssl=self._get_ssl_parameters())
ssl=self._get_ssl_parameters(),
connect_timeout=60)
cursor = connection.cursor()
logger.debug("MySQL running query: %s", query)
cursor.execute(query)
data = cursor.fetchall()
while cursor.nextset():
data = cursor.fetchall()
# TODO - very similar to pg.py
if cursor.description is not None:
columns = self.fetch_columns([(i[0], types_map.get(i[1], None)) for i in cursor.description])

View File

@ -14,18 +14,14 @@ try:
cx_Oracle.LOB: TYPE_STRING,
cx_Oracle.FIXED_CHAR: TYPE_STRING,
cx_Oracle.FIXED_NCHAR: TYPE_STRING,
cx_Oracle.FIXED_UNICODE: TYPE_STRING,
cx_Oracle.INTERVAL: TYPE_DATETIME,
cx_Oracle.LONG_NCHAR: TYPE_STRING,
cx_Oracle.LONG_STRING: TYPE_STRING,
cx_Oracle.LONG_UNICODE: TYPE_STRING,
cx_Oracle.NATIVE_FLOAT: TYPE_FLOAT,
cx_Oracle.NCHAR: TYPE_STRING,
cx_Oracle.NUMBER: TYPE_FLOAT,
cx_Oracle.ROWID: TYPE_INTEGER,
cx_Oracle.STRING: TYPE_STRING,
cx_Oracle.TIMESTAMP: TYPE_DATETIME,
cx_Oracle.UNICODE: TYPE_STRING,
}

View File

@ -69,6 +69,11 @@ class PostgreSQL(BaseSQLQueryRunner):
"dbname": {
"type": "string",
"title": "Database Name"
},
"sslmode": {
"type": "string",
"title": "SSL Mode",
"default": "prefer"
}
},
"order": ['host', 'port', 'user', 'password'],

View File

@ -159,7 +159,7 @@ class Salesforce(BaseQueryRunner):
data = {'columns': columns, 'rows': rows}
json_data = json_dumps(data)
except SalesforceError as err:
error = err.message
error = err.content
json_data = None
return json_data, error

View File

@ -19,6 +19,7 @@ from redash import settings
COMMENTS_REGEX = re.compile("/\*.*?\*/")
WRITER_ENCODING = os.environ.get('REDASH_CSV_WRITER_ENCODING', 'utf-8')
WRITER_ERRORS = os.environ.get('REDASH_CSV_WRITER_ERRORS', 'strict')
def utcnow():
@ -113,7 +114,7 @@ class UnicodeWriter:
def _encode_utf8(self, val):
if isinstance(val, (unicode, str)):
return val.encode(WRITER_ENCODING)
return val.encode(WRITER_ENCODING, WRITER_ERRORS)
return val

View File

@ -10,7 +10,7 @@ pyOpenSSL==16.2.0
vertica-python==0.5.1
td-client==0.8.0
pymssql==2.1.3
dql==0.5.16
dql==0.5.24
dynamo3==0.4.7
botocore==1.5.72
sasl>=0.1.3

View File

@ -1,4 +1,4 @@
# Requires installation of, or similar versions of:
# oracle-instantclient12.1-basic_12.1.0.2.0-2_amd64.deb
# oracle-instantclient12.1-devel_12.1.0.2.0-2_amd64.deb
cx_Oracle==5.2
# oracle-instantclient12.2-basic_12.2.0.1.0-1_x86_64.rpm
# oracle-instantclient12.2-devel_12.2.0.1.0-1_x64_64.rpm
cx_Oracle==5.3