提交 35b376aa 编写于 作者: V Vlad Ilyushchenko

initial cut of query export

上级 281bd803
......@@ -5,19 +5,32 @@
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (c) 2014-2016 Appsicle
* Copyright (C) 2014-2016 Appsicle
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* http://www.apache.org/licenses/LICENSE-2.0
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the GNU Affero General Public License in all respects for
* all of the code used other than as permitted herein. If you modify file(s)
* with this exception, you may extend this exception to your version of the
* file(s), but you are not obligated to do so. If you do not wish to do so,
* delete this exception statement from your version. If you delete this
* exception statement from all source files in the program, then also delete
* it in the license file.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.questdb;
......@@ -32,10 +45,7 @@ import com.questdb.net.http.HttpServer;
import com.questdb.net.http.HttpServerConfiguration;
import com.questdb.net.http.MimeTypes;
import com.questdb.net.http.SimpleUrlMatcher;
import com.questdb.net.http.handlers.ExistenceCheckHandler;
import com.questdb.net.http.handlers.ImportHandler;
import com.questdb.net.http.handlers.QueryHandler;
import com.questdb.net.http.handlers.StaticContentHandler;
import com.questdb.net.http.handlers.*;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
......@@ -80,8 +90,10 @@ class BootstrapMain {
final SimpleUrlMatcher matcher = new SimpleUrlMatcher();
JournalFactory factory = new JournalFactory(configuration.getDbPath().getAbsolutePath());
JournalFactoryPool pool = new JournalFactoryPool(factory.getConfiguration(), configuration.getJournalPoolSize());
matcher.put("/imp", new ImportHandler(factory));
matcher.put("/js", new QueryHandler(new JournalFactoryPool(factory.getConfiguration(), configuration.getJournalPoolSize())));
matcher.put("/js", new QueryHandler(pool));
matcher.put("/csv", new ExportHandler(pool));
matcher.put("/chk", new ExistenceCheckHandler(factory));
matcher.setDefaultHandler(new StaticContentHandler(configuration.getHttpPublic(), new MimeTypes(configuration.getMimeTypes())));
......
/*******************************************************************************
* ___ _ ____ ____
* / _ \ _ _ ___ ___| |_| _ \| __ )
* | | | | | | |/ _ \/ __| __| | | | _ \
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (C) 2014-2016 Appsicle
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the GNU Affero General Public License in all respects for
* all of the code used other than as permitted herein. If you modify file(s)
* with this exception, you may extend this exception to your version of the
* file(s), but you are not obligated to do so. If you do not wish to do so,
* delete this exception statement from your version. If you delete this
* exception statement from all source files in the program, then also delete
* it in the license file.
*
******************************************************************************/
package com.questdb.net.http.handlers;
import com.questdb.ex.*;
import com.questdb.factory.JournalCachingFactory;
import com.questdb.factory.JournalFactoryPool;
import com.questdb.factory.configuration.RecordColumnMetadata;
import com.questdb.factory.configuration.RecordMetadata;
import com.questdb.log.Log;
import com.questdb.log.LogFactory;
import com.questdb.log.LogRecord;
import com.questdb.misc.Chars;
import com.questdb.misc.Misc;
import com.questdb.misc.Numbers;
import com.questdb.net.http.ChunkedResponse;
import com.questdb.net.http.ContextHandler;
import com.questdb.net.http.IOContext;
import com.questdb.ql.Record;
import com.questdb.ql.RecordCursor;
import com.questdb.ql.RecordSource;
import com.questdb.ql.parser.QueryError;
import com.questdb.std.CharSink;
import com.questdb.std.LocalValue;
import com.questdb.std.Mutable;
import com.questdb.store.ColumnType;
import java.io.Closeable;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;
import static com.questdb.net.http.handlers.QueryHandler.CACHE;
import static com.questdb.net.http.handlers.QueryHandler.COMPILER;
public class ExportHandler implements ContextHandler {
private final JournalFactoryPool factoryPool;
private final LocalValue<ExportHandlerContext> localContext = new LocalValue<>();
private final AtomicLong cacheHits = new AtomicLong();
private final AtomicLong cacheMisses = new AtomicLong();
public ExportHandler(JournalFactoryPool factoryPool) {
this.factoryPool = factoryPool;
}
@Override
public void handle(IOContext context) throws IOException {
ExportHandlerContext ctx = localContext.get(context);
if (ctx == null) {
localContext.set(context, ctx = new ExportHandlerContext());
}
ctx.fd = context.channel.getFd();
// Query text.
ChunkedResponse r = context.chunkedResponse();
CharSequence query = context.request.getUrlParam("query");
if (query == null || query.length() == 0) {
ctx.info().$("Empty query request received. Sending empty reply.").$();
header(r, 200);
r.done();
return;
}
// Url Params.
long skip = 0;
long stop = Long.MAX_VALUE;
CharSequence limit = context.request.getUrlParam("limit");
if (limit != null) {
int sepPos = Chars.indexOf(limit, ',');
try {
if (sepPos > 0) {
skip = Numbers.parseLong(limit, 0, sepPos);
if (sepPos + 1 < limit.length()) {
stop = Numbers.parseLong(limit, sepPos + 1, limit.length());
}
} else {
stop = Numbers.parseLong(limit);
}
} catch (NumericException ex) {
// Skip or stop will have default value.
}
}
if (stop < 0) {
stop = 0;
}
if (skip < 0) {
skip = 0;
}
ctx.query = query;
ctx.skip = skip;
ctx.count = 0L;
ctx.stop = stop;
ctx.info().$("Query: ").$(query).
$(", skip: ").$(skip).
$(", stop: ").$(stop).$();
executeQuery(r, ctx);
resume(context);
}
@SuppressWarnings("ConstantConditions")
@Override
public void resume(IOContext context) throws IOException {
ExportHandlerContext ctx = localContext.get(context);
if (ctx == null || ctx.cursor == null) {
return;
}
final ChunkedResponse r = context.chunkedResponse();
final int columnCount = ctx.metadata.getColumnCount();
OUT:
while (true) {
try {
SWITCH:
switch (ctx.state) {
case METADATA:
for (; ctx.columnIndex < columnCount; ctx.columnIndex++) {
RecordColumnMetadata column = ctx.metadata.getColumnQuick(ctx.columnIndex);
r.bookmark();
if (ctx.columnIndex > 0) {
r.put(',');
}
r.putQuoted(column.getName());
}
r.put(Misc.EOL);
ctx.state = QueryState.RECORD_START;
// fall through
case RECORD_START:
if (ctx.record == null) {
// check if cursor has any records
while (true) {
if (ctx.cursor.hasNext()) {
ctx.record = ctx.cursor.next();
ctx.count++;
if (ctx.count > ctx.skip) {
break;
}
} else {
ctx.state = QueryState.DATA_SUFFIX;
break SWITCH;
}
}
}
if (ctx.count > ctx.stop) {
ctx.state = QueryState.DATA_SUFFIX;
break;
}
ctx.state = QueryState.RECORD_COLUMNS;
ctx.columnIndex = 0;
// fall through
case RECORD_COLUMNS:
for (; ctx.columnIndex < columnCount; ctx.columnIndex++) {
RecordColumnMetadata m = ctx.metadata.getColumnQuick(ctx.columnIndex);
r.bookmark();
if (ctx.columnIndex > 0) {
r.put(',');
}
putValue(r, m.getType(), ctx.record, ctx.columnIndex);
}
r.put(Misc.EOL);
ctx.record = null;
ctx.state = QueryState.RECORD_START;
break;
case DATA_SUFFIX:
sendDone(r, ctx);
break OUT;
default:
break OUT;
}
} catch (ResponseContentBufferTooSmallException ignored) {
if (r.resetToBookmark()) {
r.sendChunk();
} else {
// what we have here is out unit of data, column value or query
// is larger that response content buffer
// all we can do in this scenario is to log appropriately
// and disconnect socket
ctx.info().$("Response buffer is too small, state=").$(ctx.state).$();
throw DisconnectedChannelException.INSTANCE;
}
}
}
}
private static void sendException(ChunkedResponse r, int position, CharSequence message, int status) throws DisconnectedChannelException, SlowWritableChannelException {
header(r, status);
r.put("Error at(").put(position).put("): ").put(message).put(Misc.EOL);
r.sendChunk();
r.done();
}
private static void putValue(CharSink sink, ColumnType type, Record rec, int col) {
switch (type) {
case BOOLEAN:
sink.put(rec.getBool(col));
break;
case BYTE:
sink.put(rec.get(col));
break;
case DOUBLE:
double d = rec.getDouble(col);
if (d == d) {
sink.put(d, 10);
}
break;
case FLOAT:
float f = rec.getFloat(col);
if (f == f) {
sink.put(f, 10);
}
break;
case INT:
final int i = rec.getInt(col);
if (i > Integer.MIN_VALUE) {
Numbers.append(sink, i);
}
break;
case LONG:
final long l = rec.getLong(col);
if (l > Long.MIN_VALUE) {
sink.put(l);
}
break;
case DATE:
final long dt = rec.getDate(col);
if (dt > Long.MIN_VALUE) {
sink.put('"').putISODate(dt).put('"');
}
break;
case SHORT:
sink.put(rec.getShort(col));
break;
case STRING:
CharSequence cs;
cs = rec.getFlyweightStr(col);
if (cs != null) {
sink.put(cs);
}
break;
case SYMBOL:
cs = rec.getSym(col);
if (cs != null) {
sink.put(cs);
}
break;
case BINARY:
break;
default:
break;
}
}
private static void header(ChunkedResponse r, int code) throws DisconnectedChannelException, SlowWritableChannelException {
r.status(code, "text/csv; charset=utf-8");
r.headers().put("Content-Disposition: attachment; filename=\"questdb-query-").put(System.currentTimeMillis()).put(".csv\"").put(Misc.EOL);
r.sendHeader();
}
private void executeQuery(ChunkedResponse r, ExportHandlerContext ctx) throws IOException {
try {
// Prepare Context.
JournalCachingFactory factory = factoryPool.get();
ctx.factory = factory;
ctx.recordSource = CACHE.get().poll(ctx.query);
if (ctx.recordSource == null) {
ctx.recordSource = COMPILER.get().compileSource(factory, ctx.query);
cacheMisses.incrementAndGet();
} else {
ctx.recordSource.reset();
cacheHits.incrementAndGet();
}
ctx.cursor = ctx.recordSource.prepareCursor(factory);
ctx.metadata = ctx.cursor.getMetadata();
ctx.state = QueryState.METADATA;
ctx.columnIndex = 0;
header(r, 200);
} catch (ParserException e) {
ctx.info().$("Parser error executing query ").$(ctx.query).$(": at (").$(QueryError.getPosition()).$(") ").$(QueryError.getMessage()).$();
sendException(r, QueryError.getPosition(), QueryError.getMessage(), 400);
} catch (JournalException e) {
ctx.error().$("Server error executing query ").$(ctx.query).$(e).$();
sendException(r, 0, e.getMessage(), 500);
} catch (InterruptedException e) {
ctx.error().$("Error executing query. Server is shutting down. Query: ").$(ctx.query).$(e).$();
sendException(r, 0, "Server is shutting down.", 500);
}
}
private void sendDone(ChunkedResponse r, ExportHandlerContext ctx) throws DisconnectedChannelException, SlowWritableChannelException {
if (ctx.count > -1) {
ctx.count = -1;
r.sendChunk();
}
r.done();
}
private enum QueryState {
METADATA, RECORD_START, RECORD_COLUMNS, DATA_SUFFIX
}
private static class ExportHandlerContext implements Mutable, Closeable {
private static final Log LOG = LogFactory.getLog(ExportHandlerContext.class);
private RecordSource recordSource;
private CharSequence query;
private RecordMetadata metadata;
private RecordCursor cursor;
private long count;
private long skip;
private long stop;
private Record record;
private JournalCachingFactory factory;
private long fd;
private QueryState state = QueryState.METADATA;
private int columnIndex;
@Override
public void clear() {
debug().$("Cleaning context").$();
metadata = null;
cursor = null;
record = null;
debug().$("Closing journal factory").$();
factory = Misc.free(factory);
if (recordSource != null) {
CACHE.get().put(query.toString(), recordSource);
recordSource = null;
}
query = null;
state = QueryState.METADATA;
}
@Override
public void close() throws IOException {
debug().$("Closing context").$();
clear();
}
private LogRecord debug() {
return LOG.debug().$('[').$(fd).$("] ");
}
private LogRecord error() {
return LOG.error().$('[').$(fd).$("] ");
}
private LogRecord info() {
return LOG.info().$('[').$(fd).$("] ");
}
}
}
......@@ -63,13 +63,13 @@ import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;
public class QueryHandler implements ContextHandler {
private static final ThreadLocal<QueryCompiler> COMPILER = new ThreadLocal<>(new ObjectFactory<QueryCompiler>() {
public static final ThreadLocal<QueryCompiler> COMPILER = new ThreadLocal<>(new ObjectFactory<QueryCompiler>() {
@Override
public QueryCompiler newInstance() {
return new QueryCompiler();
}
});
private static final ThreadLocal<AssociativeCache<RecordSource>> CACHE = new ThreadLocal<>(new ObjectFactory<AssociativeCache<RecordSource>>() {
public static final ThreadLocal<AssociativeCache<RecordSource>> CACHE = new ThreadLocal<>(new ObjectFactory<AssociativeCache<RecordSource>>() {
@Override
public AssociativeCache<RecordSource> newInstance() {
return new AssociativeCache<>(8, 128);
......
......@@ -29,4 +29,4 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~--><!--suppress HtmlUnknownTag --><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>QuestDB - Console</title><link rel="apple-touch-icon" href="apple-touch-icon.png"><!-- Place favicon.ico in the root directory --><link rel="stylesheet" href="styles/qdb.css"><script type="text/javascript">history.pushState(null, null, 'index.html');
window.addEventListener('popstate', function () {
history.pushState(null, null, 'index.html');
});</script></head><body class="mini-navbar pace-done"><div id="wrapper"><nav class="navbar-default navbar-static-side" role="navigation"><div class="sidebar-collapse"><ul class="nav metismenu" id="side-menu"><li class="nav-header"><div class="dropdown profile-element"><a data-toggle="dropdown" class="dropdown-toggle" href="#"><span class="clear"><h3>QuestDB</h3></span></a></div><div class="logo-element">QDB</div></li><li><a id="sql-editor" href="#"><i class="fa fa-table"></i> <span class="nav-label">SQL Console</span></a></li><li><a id="file-upload" href="#"><i class="fa fa-upload"></i> <span class="nav-label">Data import</span></a></li><li><a href="#"><i class="fa fa-pie-chart"></i> <span class="nav-label">Analytics</span></a></li></ul></div></nav><div id="page-wrapper" class="gray-bg"><div class="row border-bottom"><nav class="navbar navbar-static-top" role="navigation" style="margin-bottom: 0"><div class="navbar-header"><a class="navbar-minimalize minimalize-styl-2 btn btn-primary" href="#"><i class="fa fa-bars"></i></a></div><ul class="nav navbar-top-links navbar-right"><li><span class="m-r-sm text-muted welcome-message">Welcome to QuestDB Console</span></li><li><a href="#"><i class="fa fa-sign-out"></i> Log out</a></li></ul></nav></div><div class="row wrapper border-bottom white-bg page-heading js-sql-panel"><div class="col-lg-10"><h3>SQL Console</h3></div></div><div class="wrapper wrapper-content js-sql-panel"><div class="row"><div class="col-lg-12"><div class="ibox"><div class="ibox-content"><div class="m-b-sm"><div class="btn-group pull-right"><button class="btn btn-white btn-sm"><i class="fa fa-arrow-left"></i></button></div><button class="btn btn-white btn-sm m-r-sm js-query-run"><i class="fa fa-play"></i>Run</button> <button class="btn btn-white btn-sm js-editor-copy"><i class="fa fa-copy"></i></button> <button class="btn btn-white btn-sm"><i class="fa fa-scissors"></i></button> <button class="btn btn-white btn-sm m-r-sm"><i class="fa fa-clipboard"></i></button> <button data-toggle="button" type="button" class="btn btn-white btn-sm js-editor-toggle-invisible"><i class="fa fa-paragraph"></i></button></div><!--<div id="debug"></div>--><div id="sqlEditor" class="editor border-rounded-top-half"></div><div class="query-progress-spinner js-query-spinner"></div><div class="query-message query-message-ok js-query-message-panel"><div class="query-time"><i class="fa fa-clock-o"></i><div class="js-query-time">-</div></div><div class="js-query-message-text"></div></div></div></div></div></div><div class="row"><div class="col-lg-12"><div class="ibox"><div class="ibox-content"><div id="grid"><div class="qg-header-row"></div><div class="qg-viewport"><div class="qg-canvas"></div></div></div></div></div></div></div></div><div class="row wrapper border-bottom white-bg page-heading js-import-panel"><div class="col-lg-10"><h3>Data import</h3></div></div><div class="wrapper wrapper-content js-import-panel"><div class="row"><div class="col-lg-12"><div class="ibox"><div class="ibox-content"><div id="dragTarget" class="drag-target drag-idle"><h2>Drag files here to import</h2></div><div class="m-b-sm"><button id="btnImportClearSelected" class="btn btn-white btn-sm" disabled="disabled"><i class="fa fa-remove"></i>Clear</button> <button id="btnRetry" class="btn btn-white btn-sm" title disabled="disabled"><i class="fa fa-upload"></i></button> <button id="btnImportCancel" class="btn btn-white btn-sm" title disabled="disabled"><i class="fa fa-stop"></i></button></div><div id="import-file-list"></div></div></div></div></div><div class="row" id="import-detail"><div class="col-lg-12"><div class="js-import-editor"><div class="ibox"><div class="ibox-content"><div class="row"><div class="col-lg-12"><div class="stats-switcher-viewport"><div class="stats-switcher"><div class="row vertical-align import-imported-table"><div class="col-xs-3"><i class="fa fa-table fa-2x"></i></div><div class="col-xs-9 text-right"><h3 class="font-bold js-import-tab-name">ABCD</h3></div></div><div class="import-stats"><div class="import-imported-stats pull-right"><i class="fa fa-thumbs-o-down"></i>&nbsp;<span class="js-rejected-row-count">1023003</span> rows</div><div class="import-imported-stats"><i class="fa fa-thumbs-o-up"></i>&nbsp;<span class="js-imported-row-count">15000000</span> rows</div></div></div></div></div><div class="row"><div class="col-lg-12"><div class="import-stats-chart"><div class="import-rejected pull-right"></div><div class="import-imported"></div></div></div></div><div class="row"><div class="col-lg-12"><div class="grid"><div class="ud-header-row"><div class="ud-header gh-1"><i class="fa fa-hashtag"></i></div><div class="ud-header gh-2">Column name</div><div class="ud-header gh-3">Type</div><div class="ud-header gh-4">Errors</div></div><div class="ud-canvas"></div></div></div></div></div></div></div></div><div class="panel panel-danger js-import-error"><div class="panel-heading">Import failed</div><div class="panel-body"><div class="col-sm-7 js-message">Server rejected file due to unsupported file format.</div><div class="col-sm-5 ud-btn-group js-import-error-btn-group"><form method="get"><label><input type="radio" name="importAction" value="append" class="js-btn-append"> Append</label><label><input type="radio" name="importAction" value="overwrite" class="js-btn-overwrite"> Overwrite</label><label><input type="radio" name="importAction" value="cancel" class="js-btn-cancel"> Cancel</label></form></div></div></div></div></div></div><div class="footer"><div><strong>Copyright</strong> Appsicle Ltd. &copy; 2014-2016</div></div></div></div><script src="scripts/qdb.js"></script></body></html>
\ No newline at end of file
});</script></head><body class="mini-navbar pace-done"><div id="wrapper"><nav class="navbar-default navbar-static-side" role="navigation"><div class="sidebar-collapse"><ul class="nav metismenu" id="side-menu"><li class="nav-header"><div class="dropdown profile-element"><a data-toggle="dropdown" class="dropdown-toggle" href="#"><span class="clear"><h3>QuestDB</h3></span></a></div><div class="logo-element">QDB</div></li><li><a id="sql-editor" href="#"><i class="fa fa-table"></i> <span class="nav-label">SQL Console</span></a></li><li><a id="file-upload" href="#"><i class="fa fa-upload"></i> <span class="nav-label">Data import</span></a></li><li><a href="#"><i class="fa fa-pie-chart"></i> <span class="nav-label">Analytics</span></a></li></ul></div></nav><div id="page-wrapper" class="gray-bg"><div class="row border-bottom"><nav class="navbar navbar-static-top" role="navigation" style="margin-bottom: 0"><div class="navbar-header"><a class="navbar-minimalize minimalize-styl-2 btn btn-primary" href="#"><i class="fa fa-bars"></i></a></div><ul class="nav navbar-top-links navbar-right"><li><span class="m-r-sm text-muted welcome-message">Welcome to QuestDB Console</span></li><li><a href="#"><i class="fa fa-sign-out"></i> Log out</a></li></ul></nav></div><div class="row wrapper border-bottom white-bg page-heading js-sql-panel"><div class="col-lg-10"><h3>SQL Console</h3></div></div><div class="wrapper wrapper-content js-sql-panel"><div class="row"><div class="col-lg-12"><div class="ibox"><div class="ibox-content"><div class="m-b-sm"><div class="btn-group pull-right"><button class="btn btn-white btn-sm"><i class="fa fa-arrow-left"></i></button></div><button class="btn btn-white btn-sm m-r-sm js-query-run"><i class="fa fa-play"></i>Run</button> <button class="btn btn-white btn-sm js-query-export"><i class="fa fa-download"></i></button> <button class="btn btn-white btn-sm"><i class="fa fa-scissors"></i></button> <button class="btn btn-white btn-sm m-r-sm"><i class="fa fa-clipboard"></i></button> <button data-toggle="button" type="button" class="btn btn-white btn-sm js-editor-toggle-invisible"><i class="fa fa-paragraph"></i></button></div><!--<div id="debug"></div>--><div id="sqlEditor" class="editor border-rounded-top-half"></div><div class="query-progress-spinner js-query-spinner"></div><div class="query-message query-message-ok js-query-message-panel"><div class="query-time"><i class="fa fa-clock-o"></i><div class="js-query-time">-</div></div><div class="js-query-message-text"></div></div></div></div></div></div><div class="row"><div class="col-lg-12"><div class="ibox"><div class="ibox-content"><div id="grid"><div class="qg-header-row"></div><div class="qg-viewport"><div class="qg-canvas"></div></div></div></div></div></div></div></div><div class="row wrapper border-bottom white-bg page-heading js-import-panel"><div class="col-lg-10"><h3>Data import</h3></div></div><div class="wrapper wrapper-content js-import-panel"><div class="row"><div class="col-lg-12"><div class="ibox"><div class="ibox-content"><div id="dragTarget" class="drag-target drag-idle"><h2>Drag files here to import</h2></div><div class="m-b-sm"><button id="btnImportClearSelected" class="btn btn-white btn-sm" disabled="disabled"><i class="fa fa-remove"></i>Clear</button> <button id="btnRetry" class="btn btn-white btn-sm" title disabled="disabled"><i class="fa fa-upload"></i></button> <button id="btnImportCancel" class="btn btn-white btn-sm" title disabled="disabled"><i class="fa fa-stop"></i></button></div><div id="import-file-list"></div></div></div></div></div><div class="row" id="import-detail"><div class="col-lg-12"><div class="js-import-editor"><div class="ibox"><div class="ibox-content"><div class="row"><div class="col-lg-12"><div class="stats-switcher-viewport"><div class="stats-switcher"><div class="row vertical-align import-imported-table"><div class="col-xs-3"><i class="fa fa-table fa-2x"></i></div><div class="col-xs-9 text-right"><h3 class="font-bold js-import-tab-name">ABCD</h3></div></div><div class="import-stats"><div class="import-imported-stats pull-right"><i class="fa fa-thumbs-o-down"></i>&nbsp;<span class="js-rejected-row-count">1023003</span> rows</div><div class="import-imported-stats"><i class="fa fa-thumbs-o-up"></i>&nbsp;<span class="js-imported-row-count">15000000</span> rows</div></div></div></div></div><div class="row"><div class="col-lg-12"><div class="import-stats-chart"><div class="import-rejected pull-right"></div><div class="import-imported"></div></div></div></div><div class="row"><div class="col-lg-12"><div class="grid"><div class="ud-header-row"><div class="ud-header gh-1"><i class="fa fa-hashtag"></i></div><div class="ud-header gh-2">Column name</div><div class="ud-header gh-3">Type</div><div class="ud-header gh-4">Errors</div></div><div class="ud-canvas"></div></div></div></div></div></div></div></div><div class="panel panel-danger js-import-error"><div class="panel-heading">Import failed</div><div class="panel-body"><div class="col-sm-7 js-message">Server rejected file due to unsupported file format.</div><div class="col-sm-5 ud-btn-group js-import-error-btn-group"><form method="get"><label><input type="radio" name="importAction" value="append" class="js-btn-append"> Append</label><label><input type="radio" name="importAction" value="overwrite" class="js-btn-overwrite"> Overwrite</label><label><input type="radio" name="importAction" value="cancel" class="js-btn-cancel"> Cancel</label></form></div></div></div></div></div></div><div class="footer"><div><strong>Copyright</strong> Appsicle Ltd. &copy; 2014-2016</div></div></div></div><script src="scripts/qdb.js"></script></body></html>
\ No newline at end of file
......@@ -126,7 +126,8 @@
</div>
<button class="btn btn-white btn-sm m-r-sm js-query-run"><i class="fa fa-play"></i>Run
</button>
<button class="btn btn-white btn-sm js-editor-copy"><i class="fa fa-copy"></i></button>
<button class="btn btn-white btn-sm js-query-export"><i class="fa fa-download"></i>
</button>
<button class="btn btn-white btn-sm"><i class="fa fa-scissors"></i></button>
<button class="btn btn-white btn-sm m-r-sm"><i class="fa fa-clipboard"></i></button>
<button data-toggle="button" type="button"
......
......@@ -622,6 +622,12 @@
canvas.bind('keydown', onKeyDown);
$(document).on('query.ok', update);
$(window).resize(resize);
$('.js-query-export').click(function (e) {
e.preventDefault();
if (query) {
window.location.href = '/csv?query=' + encodeURIComponent(query);
}
});
}
bind();
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册