mirror of
https://github.com/valitydev/redash.git
synced 2024-11-08 09:53:59 +00:00
Merged with upstream
This commit is contained in:
commit
04ddb289ee
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
||||
|
@ -15,13 +15,18 @@ const EditTextBoxComponent = {
|
||||
this.widget = this.resolve.widget;
|
||||
this.saveWidget = () => {
|
||||
this.saveInProgress = true;
|
||||
this.widget.$save().then(() => {
|
||||
if (this.widget.new_text !== this.widget.existing_text) {
|
||||
this.widget.text = this.widget.new_text;
|
||||
this.widget.$save().then(() => {
|
||||
this.close();
|
||||
}).catch(() => {
|
||||
toastr.error('Widget can not be updated');
|
||||
}).finally(() => {
|
||||
this.saveInProgress = false;
|
||||
});
|
||||
} else {
|
||||
this.close();
|
||||
}).catch(() => {
|
||||
toastr.error('Widget can not be updated');
|
||||
}).finally(() => {
|
||||
this.saveInProgress = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
@ -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: {
|
||||
|
@ -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)) {
|
||||
|
@ -134,17 +134,23 @@
|
||||
|
||||
<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>
|
||||
|
||||
<div class="checkbox" ng-if="options.globalSeriesType == 'custom'">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="options.enableConsoleLogs">
|
||||
<i class="input-helper"></i> Show errors in the console
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<input type="checkbox" ng-model="options.enableConsoleLogs">
|
||||
<i class="input-helper"></i> Show errors in the console
|
||||
</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">
|
||||
|
@ -87,10 +87,12 @@ function ChartEditor(ColorPalette, clientConfig) {
|
||||
|
||||
scope.showSizeColumnPicker = () => some(scope.options.seriesOptions, options => options.type === 'bubble');
|
||||
|
||||
scope.options.customCode = `// Available variables are x, ys, element, and Plotly
|
||||
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();
|
||||
|
@ -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 {
|
||||
refresh();
|
||||
if (scope.options.autoRedraw) {
|
||||
refresh();
|
||||
}
|
||||
} catch (err) {
|
||||
if (scope.options.enableConsoleLogs) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -1,34 +1,77 @@
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-lg-6">Counter Value Column Name</label>
|
||||
<div class="col-lg-6">
|
||||
<select ng-options="name for name in queryResult.getColumnNames()" ng-model="visualization.options.counterColName" class="form-control" ng-disabled="visualization.options.countRow"></select>
|
||||
<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">
|
||||
<select ng-options="name for name in queryResult.getColumnNames()" ng-model="visualization.options.counterColName" class="form-control" ng-disabled="visualization.options.countRow"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-6">Counter Value Row Number</label>
|
||||
<div class="col-lg-6">
|
||||
<input type="number" ng-model="visualization.options.rowNumber" class="form-control" ng-disabled="visualization.options.countRow">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-6">Target Value Column Name</label>
|
||||
<div class="col-lg-6">
|
||||
<select ng-options="name for name in queryResult.getColumnNames()" ng-model="visualization.options.targetColName" class="form-control">
|
||||
<option value="">No target value</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="visualization.options.targetColName">
|
||||
<label class="col-lg-6">Target Value Row Number</label>
|
||||
<div class="col-lg-6">
|
||||
<input type="number" ng-model="visualization.options.targetRowNumber" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-lg-6">
|
||||
<input type="checkbox" ng-model="visualization.options.countRow">
|
||||
<i class="input-helper"></i> Count Rows
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-6">Counter Value Row Number</label>
|
||||
<div class="col-lg-6">
|
||||
<input type="number" ng-model="visualization.options.rowNumber" class="form-control" ng-disabled="visualization.options.countRow">
|
||||
|
||||
<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>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-6">Target Value Column Name</label>
|
||||
<div class="col-lg-6">
|
||||
<select ng-options="name for name in queryResult.getColumnNames()" ng-model="visualization.options.targetColName" class="form-control">
|
||||
<option value="">No target value</option>
|
||||
</select>
|
||||
<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>
|
||||
<div class="form-group" ng-if="visualization.options.targetColName">
|
||||
<label class="col-lg-6">Target Value Row Number</label>
|
||||
<div class="col-lg-6">
|
||||
<input type="number" ng-model="visualization.options.targetRowNumber" class="form-control">
|
||||
<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>
|
||||
<div class="form-group">
|
||||
<div class="col-lg-6">
|
||||
<input type="checkbox" ng-model="visualization.options.countRow">
|
||||
<i class="input-helper"></i> Count Rows
|
||||
<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>
|
||||
|
@ -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>
|
||||
|
@ -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({
|
||||
|
@ -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,15 +71,31 @@ 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)
|
||||
results = json.loads(results)
|
||||
|
||||
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': []}
|
||||
|
@ -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])
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
@ -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'],
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user