adhoq 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -6
  3. data/app/assets/javascripts/adhoq/current_tables.js.coffee +17 -3
  4. data/app/assets/javascripts/adhoq/previewer.js.coffee +10 -1
  5. data/app/assets/stylesheets/adhoq/adhoq.css.sass +38 -8
  6. data/app/controllers/adhoq/application_controller.rb +2 -0
  7. data/app/controllers/adhoq/current_tables_controller.rb +4 -1
  8. data/app/controllers/adhoq/executions_controller.rb +24 -3
  9. data/app/controllers/adhoq/explains_controller.rb +1 -1
  10. data/app/controllers/adhoq/previews_controller.rb +1 -1
  11. data/app/controllers/adhoq/queries_controller.rb +1 -1
  12. data/app/decorators/adhoq/execution_decorator.rb +20 -0
  13. data/app/decorators/adhoq/query_decorator.rb +9 -0
  14. data/app/helpers/adhoq/application_helper.rb +12 -2
  15. data/app/jobs/adhoq/execute_job.rb +11 -0
  16. data/app/models/adhoq/execution.rb +4 -6
  17. data/app/models/adhoq/report.rb +8 -0
  18. data/app/views/adhoq/current_tables/index.html.slim +10 -8
  19. data/app/views/adhoq/queries/_current_tables_leftbar.html.slim +9 -0
  20. data/app/views/adhoq/queries/_execution.html.slim +10 -0
  21. data/app/views/adhoq/queries/_form.html.slim +49 -30
  22. data/app/views/adhoq/queries/_queries.html.slim +14 -0
  23. data/app/views/adhoq/queries/_query.html.slim +20 -24
  24. data/app/views/adhoq/queries/edit.html.slim +11 -2
  25. data/app/views/adhoq/queries/index.html.slim +11 -1
  26. data/app/views/adhoq/queries/new.html.slim +10 -2
  27. data/app/views/adhoq/queries/show.html.slim +11 -1
  28. data/app/views/layouts/adhoq/application.html.slim +1 -4
  29. data/db/migrate/20141003095645_create_adhoq_queries.rb +1 -1
  30. data/db/migrate/20141006014750_create_adhoq_executions.rb +1 -1
  31. data/db/migrate/20141007052308_create_adhoq_reports.rb +1 -1
  32. data/lib/adhoq.rb +1 -0
  33. data/lib/adhoq/configuration.rb +10 -0
  34. data/lib/adhoq/engine.rb +1 -0
  35. data/lib/adhoq/executor.rb +4 -28
  36. data/lib/adhoq/executor/connection_wrapper.rb +32 -0
  37. data/lib/adhoq/global_variable.rb +1 -0
  38. data/lib/adhoq/storage.rb +1 -0
  39. data/lib/adhoq/storage/fog_storage.rb +4 -0
  40. data/lib/adhoq/storage/on_the_fly.rb +32 -0
  41. data/lib/adhoq/storage/s3.rb +26 -2
  42. data/lib/adhoq/version.rb +1 -1
  43. data/spec/adhoq/executor/connection_wrapper_spec.rb +16 -0
  44. data/spec/adhoq/executor_spec.rb +0 -13
  45. data/spec/adhoq/storage_spec.rb +14 -0
  46. data/spec/models/adhoq/execution_spec.rb +21 -0
  47. data/spec/support/activejob_helper.rb +26 -0
  48. metadata +43 -4
  49. data/app/views/adhoq/application/_sidebar_queries_index.html.slim +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9e0a340afd6e9ff7f1d8ba89b6b85f206dcb7f74
4
- data.tar.gz: 39043bf804ec87a7183aaa331a1858ea6fc27d86
3
+ metadata.gz: e44fcb3a79e299b2a337652f8439d58cd4babe71
4
+ data.tar.gz: 5312feb5fee5890cae4b27f48c541a36b937ef0e
5
5
  SHA512:
6
- metadata.gz: fd569b25d51fff86895925ecf0774c01b6cf48f8a6b1ee533b230caf5f19ceaac35be0ea70ef9473fd8a9e82ba8cec21cd3bae58f1be30f1f0bc0ccdb805650e
7
- data.tar.gz: 2bd75bfaaaee3ca6ede743e59b334f198ec74a4d8a2fed174e949ca2c9abdd113d0d0810be83e9a143e55ba4d542db660a2a5d27f3ef9c6a0abfbbcf97705c1d
6
+ metadata.gz: 19546d4c89ab4cbb7da38b46e02a7cc4bfe091d1d00bbd3110d186a26586f929cdccdab8bf2bf12f322cfaf052297d102eefcfe3660c47012da952a0464baae9
7
+ data.tar.gz: 914e24aabd3a6a896c62e3c276642f8a6ba37d85d8d6f285363e5c1dbf6b4a3025f714f065989475b4042edfb8ab513ef2462974fb45214fa754c5f640ef345e
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- Adhoq [![Build Status](https://travis-ci.org/esminc/adhoq.svg)](https://travis-ci.org/esminc/adhoq) [![Code Climate](https://codeclimate.com/github/esminc/adhoq/badges/gpa.svg)](https://codeclimate.com/github/esminc/adhoq)
1
+ Adhoq [![Build Status](https://travis-ci.org/esminc/adhoq.svg)](https://travis-ci.org/esminc/adhoq) [![Code Climate](https://codeclimate.com/github/esminc/adhoq/badges/gpa.svg)](https://codeclimate.com/github/esminc/adhoq) [![Test Coverage](https://codeclimate.com/github/esminc/adhoq/badges/coverage.svg)](https://codeclimate.com/github/esminc/adhoq/coverage)
2
2
  ====
3
3
 
4
4
  Rails engine to generate instant reports from adhoc SQL query.
@@ -7,16 +7,16 @@ Rails engine to generate instant reports from adhoc SQL query.
7
7
 
8
8
  ## Features
9
9
 
10
- - Export ad-hoc SQL result to .xlsx file
10
+ - Export ad-hoc SQL reports in some formats:
11
+ - .csv
12
+ - .json
13
+ - .xlsx
11
14
  - Persist generated report as local file or in AWS S3
12
15
  - Rails 4.x & 3.2 support
13
16
  - Nice administration console with rails engine
14
17
 
15
18
  ### Future planning
16
19
 
17
- - Export reports in some formats:
18
- - [ ] .csv
19
- - [ ] .json
20
20
  - [ ] Label data substitution
21
21
 
22
22
  ## Installation
@@ -56,7 +56,7 @@ Rails.application.routes.draw do
56
56
  end
57
57
  ```
58
58
 
59
- Edit initialization file in `config/initializer/adhoq.rb`
59
+ Edit initialization file in `config/initializers/adhoq.rb`
60
60
 
61
61
  ```ruby
62
62
  Adhoq.configure do |config|
@@ -1,4 +1,18 @@
1
- Adhoq.loadCurrentTableTabOnce = ($el)->
2
- pane = $("#{$el.attr('href')}:has(.loading)")
1
+ loadCurrentTableTabOnce = ($el)->
2
+ $el.load($el.find('a.loading').attr('href'))
3
+
4
+ Adhoq.toggleCurrentTables = (action, elements)->
5
+ loadCurrentTableTabOnce($('#current-tables'))
6
+
7
+ $main = $(elements.main)
8
+ $tables = $(elements.tables)
9
+
10
+ if action is 'show'
11
+ $main.addClass('col-md-6').removeClass('col-md-12')
12
+ $tables.addClass('col-md-6').show()
13
+ else
14
+ $main.addClass('col-md-12').removeClass('col-md-6')
15
+ $tables.addClass('col-md-6').hide()
16
+
17
+ true
3
18
 
4
- pane.load(pane.find('a.loading').attr('href'))
@@ -2,8 +2,10 @@ class Previewer
2
2
  constructor: (@el)->
3
3
 
4
4
  init: ->
5
+ @el.on 'adhoq:updatePreview', => @update()
6
+
5
7
  @el.on 'click', =>
6
- @update()
8
+ @el.trigger 'adhoq:updatePreview'
7
9
  false
8
10
 
9
11
  update: ->
@@ -23,3 +25,10 @@ class Previewer
23
25
 
24
26
  Adhoq.enablePreview = ($el)->
25
27
  (new Previewer($el)).init()
28
+
29
+ Adhoq.enablePreviewKeybordShortCut= ($textarea, previewSelector)->
30
+ $textarea.on 'keyup', (ev)->
31
+ if(ev.ctrlKey && ev.keyCode is 82)
32
+ $(previewSelector).trigger('adhoq:updatePreview')
33
+
34
+ false
@@ -21,22 +21,41 @@ $short-span: $font-size-base / 2
21
21
  margin: 0 10px
22
22
  font-size: $font-size-base
23
23
 
24
- #sidebar
25
- h2 a.new-query
26
- float: right
24
+ #queries
25
+ a.new-query
26
+ margin-bottom: $short-span
27
27
 
28
- ul.queries
28
+ ol.queries-index
29
29
  li.panel
30
30
  margin-bottom: $short-span
31
31
 
32
- .panel-heading h2
33
- margin: 0
34
- font-size: $font-size-base
32
+ .panel-heading
33
+ padding: $short-span $short-span * 2
34
+ h2
35
+ margin: 0
36
+ font-size: $font-size-base
35
37
 
36
38
  p.panel-body.description
37
39
  margin-bottom: 0
40
+ font-size: $font-size-base * 0.9
41
+
42
+ #the-query
43
+ .page-header
44
+ margin-top: 0
45
+ h1
46
+ margin-top: 0
47
+
48
+ small
49
+ font-size: $font-size-base
38
50
 
39
51
  form.query-form
52
+ margin-bottom: 30px
53
+
54
+ h1
55
+ font-size: $font-size-base
56
+ margin-top: $font-size-base / 2
57
+ margin-bottom: $font-size-base / 4
58
+
40
59
  textarea#query_query
41
60
  font-family: monospace
42
61
 
@@ -57,10 +76,17 @@ form.query-form
57
76
  padding-left: $short-span / 2
58
77
 
59
78
  #current-tables
79
+ display: none
60
80
  font-size: $font-size-base * 0.9
61
81
 
82
+ h3
83
+ font-size: $font-size-base
84
+ margin-top: $font-size-base / 2
85
+ margin-bottom: $font-size-base / 4
86
+
87
+ small
88
+ margin-left: $font-size-base
62
89
  table
63
- width: 800px
64
90
 
65
91
  caption
66
92
  text-align: left
@@ -95,7 +121,11 @@ form.query-form
95
121
 
96
122
  .tab-pane > h3
97
123
  font-size: $font-size-base * 1.5
124
+ margin-top: $font-size-base / 2
98
125
 
99
126
  small
100
127
  font-size: $font-size-base
101
128
  margin-left: $font-size-base
129
+
130
+ .js-preview-result table, .js-explain-result pre
131
+ font-family: monospace
@@ -1,5 +1,7 @@
1
1
  module Adhoq
2
2
  class ApplicationController < ::ApplicationController
3
+ layout 'adhoq/application'
4
+
3
5
  include Adhoq::AuthorizationMethods
4
6
  end
5
7
  end
@@ -3,8 +3,11 @@ module Adhoq
3
3
  before_filter :eager_load_models
4
4
 
5
5
  def index
6
+ hidden_model_names = Array(Adhoq.config.hidden_model_names)
7
+ hidden_model_names << 'ActiveRecord::SchemaMigration'
8
+
6
9
  @ar_classes = ActiveRecord::Base.subclasses.
7
- reject {|klass| klass.name == 'ActiveRecord::SchemaMigration' }.
10
+ reject {|klass| hidden_model_names.include?(klass.name) }.
8
11
  sort_by(&:name)
9
12
 
10
13
  render layout: false
@@ -7,19 +7,40 @@ module Adhoq
7
7
  end
8
8
 
9
9
  def create
10
+ async_execution? ? asynced_create : synced_create
11
+ end
12
+
13
+ private
14
+
15
+ def synced_create
10
16
  @execution = current_query.execute!(params[:execution][:report_format])
11
17
 
12
- redirect_to current_query
18
+ if @execution.report.on_the_fly?
19
+ respond_report(@execution.report)
20
+ else
21
+ redirect_to current_query
22
+ end
13
23
  end
14
24
 
15
- private
25
+ def asynced_create
26
+ Adhoq::ExecuteJob.perform_later(current_query, params[:execution][:report_format])
27
+ redirect_to current_query
28
+ end
16
29
 
17
30
  def current_query
18
31
  @query ||= Adhoq::Query.find(params[:query_id])
19
32
  end
20
33
 
21
34
  def respond_report(report)
22
- send_data report.data, type: report.mime_type, filename: report.name, disposition: 'attachment'
35
+ if Adhoq.current_storage.direct_download?
36
+ redirect_to report.data_url
37
+ else
38
+ send_data report.data, type: report.mime_type, filename: report.name, disposition: 'attachment'
39
+ end
40
+ end
41
+
42
+ def async_execution?
43
+ Adhoq.config.async_execution? && !Adhoq.current_storage.is_a?(Adhoq::Storage::OnTheFly)
23
44
  end
24
45
  end
25
46
  end
@@ -1,5 +1,5 @@
1
1
  module Adhoq
2
- class ExplainsController < ApplicationController
2
+ class ExplainsController < Adhoq::ApplicationController
3
3
  layout false
4
4
 
5
5
  def create
@@ -1,5 +1,5 @@
1
1
  module Adhoq
2
- class PreviewsController < ApplicationController
2
+ class PreviewsController < Adhoq::ApplicationController
3
3
  layout false
4
4
 
5
5
  def create
@@ -1,5 +1,5 @@
1
1
  module Adhoq
2
- class QueriesController < ApplicationController
2
+ class QueriesController < Adhoq::ApplicationController
3
3
  def index
4
4
  @queries = Adhoq::Query.recent_first
5
5
  end
@@ -0,0 +1,20 @@
1
+ module Adhoq::ExecutionDecorator
2
+ def status_label
3
+ content_tag :span, class: ["label", status_label_class] do
4
+ status
5
+ end
6
+ end
7
+
8
+ def status_label_class
9
+ case status
10
+ when "success"
11
+ "label-success"
12
+ when "failure"
13
+ "label-danger"
14
+ when "requested"
15
+ "label-default"
16
+ else
17
+ "label-default"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ require 'rouge'
2
+
3
+ module Adhoq::QueryDecorator
4
+ def query_with_highlight
5
+ formatter = Rouge::Formatters::HTML.new(css_class: 'highlight query')
6
+ lexer = Rouge::Lexers::SQL.new
7
+ formatter.format(lexer.lex(query))
8
+ end
9
+ end
@@ -16,9 +16,19 @@ module Adhoq
16
16
  if defined?(ActiveRecord::SchemaMigration)
17
17
  ActiveRecord::SchemaMigration.maximum(:version)
18
18
  else
19
- result = Adhoq::Executor.select("SELECT MAX(version) AS current_version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}")
20
- result.rows.first
19
+ connection = Adhoq::Executor::ConnectionWrapper.new
20
+ result = connection.select("SELECT MAX(version) AS current_version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}")
21
+ result.rows.first.first
21
22
  end
22
23
  end
24
+
25
+ # TODO extract into presenter
26
+ def query_friendly_name(query)
27
+ "Query: #{query.name}"
28
+ end
29
+
30
+ def table_order_key(ar_class)
31
+ ar_class.primary_key || ar_class.columns.first.name
32
+ end
23
33
  end
24
34
  end
@@ -0,0 +1,11 @@
1
+ if defined?(ActiveJob)
2
+ class Adhoq::ExecuteJob < ActiveJob::Base
3
+ queue_as do
4
+ Adhoq.config.job_queue_name.try(:to_sym) || :default
5
+ end
6
+
7
+ def perform(query, *args)
8
+ query.execute!(*args)
9
+ end
10
+ end
11
+ end
@@ -9,6 +9,9 @@ module Adhoq
9
9
 
10
10
  def generate_report!
11
11
  build_report.generate!
12
+ update_attributes(status: :success)
13
+ rescue
14
+ update_attributes(status: :failure)
12
15
  end
13
16
 
14
17
  def name
@@ -16,12 +19,7 @@ module Adhoq
16
19
  end
17
20
 
18
21
  def success?
19
- report.try(:available?)
20
- end
21
-
22
- # TODO go decorator or view model or so
23
- def status_label
24
- success? ? :success : :failure
22
+ report.try(:available?) || status == "success"
25
23
  end
26
24
  end
27
25
  end
@@ -12,6 +12,10 @@ module Adhoq
12
12
  save!
13
13
  end
14
14
 
15
+ def on_the_fly?
16
+ storage.start_with?(Adhoq::Storage::OnTheFly::PREFIX)
17
+ end
18
+
15
19
  def available?
16
20
  identifier.present? && (storage == Adhoq.current_storage.identifier)
17
21
  end
@@ -20,6 +24,10 @@ module Adhoq
20
24
  storage.get(identifier)
21
25
  end
22
26
 
27
+ def data_url(storage = Adhoq.current_storage)
28
+ storage.get_url(self)
29
+ end
30
+
23
31
  def mime_type
24
32
  Adhoq::Reporter.lookup(execution.report_format).mime_type
25
33
  end
@@ -1,10 +1,14 @@
1
1
  h3
2
- | Current database schema
2
+ i.fa.fa-database.fa-pad-r
3
+ | Current tables
3
4
  small= "Version #{schema_version}"
5
+ .pull-right
6
+ button.close[data-trigger="toggleCurrentTables" role='close']
7
+ span[aria-hidden=true] &times;
4
8
 
5
9
  ul.list-unstyled.tables
6
10
  - @ar_classes.each do |ar_class|
7
- - first_record = ar_class.unscoped.order(:id).first
11
+ - first_record = ar_class.unscoped.order(table_order_key(ar_class)).first
8
12
 
9
13
  li.ar_class data-table-name=ar_class.table_name
10
14
  table.table.table-striped.table-hover.table-bordered
@@ -14,12 +18,11 @@ ul.list-unstyled.tables
14
18
  thead
15
19
  tr
16
20
  th.col-sm-1.pk PK
17
- th.col-sm-2.name Name
18
- th.col-sm-1.type Type
21
+ th.col-sm-3.name Name
22
+ th.col-sm-2.type Type
19
23
  th.col-sm-1.null Non-Null
20
- th.col-sm-1.limit Limit
21
- th.col-sm-2.default Default
22
- th.col-sm-4.data unscoped.first
24
+ th.col-sm-2.limit Limit
25
+ th.col-sm-3.default Default
23
26
  tbody
24
27
  - ar_class.columns.each do |column|
25
28
  tr
@@ -29,4 +32,3 @@ ul.list-unstyled.tables
29
32
  td.null.icon= column.null ? '' : icon_fa('check')
30
33
  td.limit.number= column.limit
31
34
  td= column.default
32
- td.monospace= first_record.try {|r| truncate(r.read_attribute_before_type_cast(column.name).to_s, length: 50) }
@@ -0,0 +1,9 @@
1
+ #current-tables
2
+ a.loading[href=current_tables_path]
3
+
4
+ javascript:
5
+ $(function() {
6
+ $(document).on('click', '[data-trigger="toggleCurrentTables"]', function($ev) {
7
+ Adhoq.toggleCurrentTables($($ev.target).attr('role'), {main: '#main', tables: '#current-tables'});
8
+ })
9
+ });
@@ -0,0 +1,10 @@
1
+ tr[exec]
2
+ td.wip
3
+ td.created_at= exec.created_at.localtime.iso8601
4
+ td.status
5
+ = exec.status_label
6
+ td.report
7
+ - if exec.success?
8
+ = link_to(query_execution_path(query, exec, format: exec.report_format), class: 'btn btn-sm btn-default') do
9
+ i.fa.fa-download.fa-pad-r
10
+ = exec.report_format
@@ -1,28 +1,54 @@
1
- = form_for query, html: {class: 'form form-horizontal query-form', role: 'form'} do |f|
1
+ = form_for query, html: {class: 'form query-form', role: 'form'} do |f|
2
2
  .page-header
3
3
  h1
4
- = title
5
- .actions.btn-in-header
6
- button.btn.btn-primary
7
- i.fa.fa-floppy-o.fa-pad-r
8
- | Save
9
- - if f.object.persisted?
10
- = link_to 'Cancel', f.object, class: 'btn btn-default'
4
+ = f.label :query, title, class: 'control-label'
5
+ .pull-right
6
+ a.btn.btn-default.btn-sm[href='#' data-trigger='toggleCurrentTables' role='show']
7
+ i.fa.fa-database.fa-pad-r
8
+ | Show tables
11
9
 
12
10
  .form-group
13
- = f.label :name, class: 'col-sm-2 control-label'
14
- .col-sm-8
15
- = f.text_field :name, class: 'form-control', required: true
11
+ = f.text_area :query, class: 'form-control', rows: 15, required: true
16
12
 
17
- .form-group
18
- = f.label :description, class: 'col-sm-2 control-label'
19
- .col-sm-8
20
- = f.text_area :description, class: 'form-control', required: true
13
+ .modal.fade#nameAndDesc[role='dialog']
14
+ .modal-dialog
15
+ .modal-content
16
+ .modal-header
17
+ button.close[data-dismiss='modal' aria-label='Close']
18
+ button.close[type='button' data-dismiss='modal' aria-label='Close']
19
+ span[aria-hidden='true'] &times;
21
20
 
22
- .form-group
23
- = f.label :query, class: 'col-sm-2 control-label'
24
- .col-sm-8
25
- = f.text_area :query, class: 'form-control', rows: 10, required: true
21
+ h4 Add name and description to query
22
+ .modal-body
23
+ .form-horizontal
24
+ .form-group
25
+ = f.label :name, class: 'control-label col-sm-2'
26
+ .col-sm-8
27
+ = f.text_field :name, class: 'form-control', required: true
28
+
29
+ .form-group
30
+ = f.label :description, class: 'control-label col-sm-2'
31
+ .col-sm-8
32
+ = f.text_area :description, class: 'form-control', required: true
33
+ .modal-footer
34
+ button.btn.btn-primary.center-block
35
+ i.fa.fa-floppy-o.fa-pad-r
36
+ | Save
37
+
38
+ .actions
39
+ - if query.persisted?
40
+ = link_to query do
41
+ i.fa.fa-arrow-left.fa-pad-r
42
+ | Cancel
43
+ - else
44
+ = link_to :queries do
45
+ i.fa.fa-arrow-left.fa-pad-r
46
+ | Back to Index
47
+
48
+ .pull-right
49
+ = link_to '#nameAndDesc', class: 'btn btn-default btn-sm', data: {toggle: 'modal', target: '#nameAndDesc'} do
50
+ i.fa.fa-floppy-o.fa-pad-r
51
+ | Save as...
26
52
 
27
53
  ul.nav.nav-tabs[role='tablist']
28
54
  li.active
@@ -33,19 +59,15 @@ ul.nav.nav-tabs[role='tablist']
33
59
  a[role='tab' data-toggle='tab' href='#explain' ]
34
60
  i.fa.fa-info.fa-pad-r
35
61
  | Explain
36
- li
37
- a[role='tab' data-toggle='tab' href='#current-tables']
38
- i.fa.fa-database.fa-pad-r
39
- | Tables
40
62
 
41
- .tab-content
63
+ #previews.tab-content
42
64
  #preview.tab-pane.active
43
65
  h3
44
66
  | Query preview
45
67
  small
46
68
  = link_to preview_path, class: 'js-preview-button', data: {source: '#query_query', result: '.js-preview-result', remote: true, method: 'POST'} do
47
69
  i.fa.fa-refresh.fa-pad-r[data-title='Refresh preview']
48
- | Reflesh
70
+ | Refresh
49
71
 
50
72
  .js-preview-result
51
73
  .alert.alert-info Preview is shown here
@@ -56,19 +78,16 @@ ul.nav.nav-tabs[role='tablist']
56
78
  small
57
79
  = link_to explain_path, class: 'js-explain-button', data: {source: '#query_query', result: '.js-explain-result', remote: true, method: 'POST'} do
58
80
  i.fa.fa-refresh.fa-pad-r[data-title='Refresh explain']
59
- | Reflesh
81
+ | Refresh
60
82
 
61
83
  .js-explain-result
62
84
  .alert.alert-info Explain result is shown here
63
85
 
64
- #current-tables.tab-pane
65
- a.loading[href=current_tables_path]
66
-
67
86
  javascript:
68
87
  $(function() {
69
88
  Adhoq.enablePreview($('#preview a.js-preview-button'));
70
89
  Adhoq.enablePreview($('#explain a.js-explain-button'));
71
90
 
72
- $('a[data-toggle="tab"]').on('show.bs.tab', function(ev) { Adhoq.loadCurrentTableTabOnce($(ev.target)) });
91
+ Adhoq.enablePreviewKeybordShortCut($('#query_query'), '#previews .tab-pane.active a:has(".fa-refresh")')
73
92
  });
74
93
 
@@ -0,0 +1,14 @@
1
+ - highlight ||= nil
2
+ section
3
+ = link_to :root, class: 'btn btn-default new-query btn-sm center-block' do
4
+ i.fa.fa-plus-square.fa-pad-r
5
+ | New query
6
+
7
+ ol.queries-index.list-unstyled
8
+ - queries.each do |query|
9
+ - css = (query == highlight) ? 'panel-success' : 'panel-default'
10
+ li.panel[query, class= css]
11
+ .panel-heading
12
+ h2= link_to query.name, query_path(query)
13
+ p.panel-body.description= query.description
14
+
@@ -2,13 +2,18 @@ section.query
2
2
  .page-header
3
3
  h1
4
4
  = query.name
5
+ .pull-right
6
+ = link_to [:edit, query], class: 'btn btn-default btn-sm' do
7
+ i.fa.fa-pencil.fa-pad-r
8
+ | Edit
9
+ .clearfix
5
10
  small= "Updated at #{l(query.updated_at, format: :short)}"
6
- = link_to [:edit, query], class: 'btn btn-default' do
7
- i.fa.fa-pencil.fa-pad-r
8
- | Edit
9
11
  p.description= query.description
10
12
 
11
- pre.query= query.query
13
+ css:
14
+ #{Rouge::Themes::Github.render(scope: '.highlight')}
15
+
16
+ = raw query.query_with_highlight
12
17
 
13
18
  section.new-execution
14
19
  h2 Create report
@@ -21,23 +26,14 @@ section.query
21
26
 
22
27
  section.past-executions
23
28
  h2 Reports
24
- .col-md-10
25
- table.executions.table.table-striped.table-hover
26
- thead
27
- tr
28
- th.wip &nbsp;
29
- th.created_at= human(Adhoq::Execution, :created_at)
30
- th.status= human(Adhoq::Execution, :status)
31
- th.report
32
- tbody
33
- - query.executions.recent_first.each do |exec|
34
- tr[exec]
35
- td.wip
36
- td.created_at= exec.created_at.localtime.iso8601
37
- td.status
38
- span.label[class=(exec.success? ? 'label-success' : 'label-danger')]= exec.status_label
39
- td.report
40
- - if exec.success?
41
- = link_to(adhoq.query_execution_path(query, exec, format: exec.report_format), class: 'btn btn-sm btn-default') do
42
- i.fa.fa-download.fa-pad-r
43
- = exec.report_format
29
+ table.executions.table.table-striped.table-hover
30
+ thead
31
+ tr
32
+ th.wip &nbsp;
33
+ th.created_at= human(Adhoq::Execution, :created_at)
34
+ th.status= human(Adhoq::Execution, :status)
35
+ th.report
36
+ tbody
37
+ - query.executions.recent_first.each do |exec|
38
+ - next if exec.report.try(:on_the_fly?)
39
+ = render 'execution', query: query, exec: exec
@@ -1,2 +1,11 @@
1
- section.edit-query
2
- = render 'form', query: @query, title: 'Edit query'
1
+ .col-md-12
2
+ ol.breadcrumb
3
+ li= link_to 'Index', :queries
4
+ li= link_to query_friendly_name(@query), @query
5
+ li.active Edit
6
+
7
+ #main.col-md-12
8
+ section.edit-query
9
+ = render 'form', query: @query, title: "Edit query > #{query_friendly_name(@query)}"
10
+
11
+ = render 'current_tables_leftbar'
@@ -1 +1,11 @@
1
- = render 'query', query: @queries.first
1
+ .col-md-12
2
+ ol.breadcrumb
3
+ li.active Index
4
+
5
+ #queries.col-md-3
6
+ section.queries
7
+ = render 'queries', queries: @queries
8
+
9
+ #the-query.col-md-9
10
+ - if first_query = @queries.first
11
+ = render 'query', query: first_query
@@ -1,2 +1,10 @@
1
- section.new-query
2
- = render 'form', query: @query, title: 'New query'
1
+ .col-md-12
2
+ ol.breadcrumb
3
+ li= link_to 'Index', :queries
4
+ li.active New query
5
+
6
+ #main.col-md-12
7
+ section.new-query
8
+ = render 'form', query: @query, title: 'New query'
9
+
10
+ = render 'current_tables_leftbar'
@@ -1 +1,11 @@
1
- = render 'query', query: @query
1
+ .col-md-12
2
+ ol.breadcrumb
3
+ li= link_to 'Index', :queries
4
+ li.active= query_friendly_name(@query)
5
+
6
+ #queries.col-md-3
7
+ section.queries
8
+ = render 'queries', queries: Adhoq::Query.recent_first, highlight: @query
9
+
10
+ #the-query.col-md-9
11
+ = render 'query', query: @query
@@ -11,8 +11,5 @@ html
11
11
  = render 'global_nav'
12
12
 
13
13
  #contents.row
14
- #sidebar.col-md-3
15
- = render 'sidebar_queries_index'
16
-
17
- #main.col-md-9= yield
14
+ = yield
18
15
 
@@ -5,7 +5,7 @@ class CreateAdhoqQueries < ActiveRecord::Migration
5
5
  t.string :description
6
6
  t.text :query
7
7
 
8
- t.timestamps
8
+ t.timestamps null: false
9
9
  end
10
10
  end
11
11
  end
@@ -7,7 +7,7 @@ class CreateAdhoqExecutions < ActiveRecord::Migration
7
7
  t.string :status, null: false, default: 'requested'
8
8
  t.text :log
9
9
 
10
- t.timestamps
10
+ t.timestamps null: false
11
11
  end
12
12
  end
13
13
  end
@@ -6,7 +6,7 @@ class CreateAdhoqReports < ActiveRecord::Migration
6
6
  t.time :generated_at, null: false
7
7
  t.string :storage, null: false
8
8
 
9
- t.timestamps
9
+ t.timestamps null: false
10
10
  end
11
11
  end
12
12
  end
data/lib/adhoq.rb CHANGED
@@ -14,5 +14,6 @@ module Adhoq
14
14
 
15
15
  configure do |config|
16
16
  config.authorization = proc { true }
17
+ config.database_connection = proc { ActiveRecord::Base.connection }
17
18
  end
18
19
  end
@@ -10,6 +10,12 @@ module Adhoq
10
10
 
11
11
  config_accessor :current_user
12
12
 
13
+ config_accessor :database_connection
14
+ config_accessor :hidden_model_names
15
+
16
+ config_accessor :async_execution
17
+ config_accessor :job_queue_name
18
+
13
19
  def callablize(name)
14
20
  if (c = config[name]).respond_to?(:call)
15
21
  c
@@ -17,5 +23,9 @@ module Adhoq
17
23
  c.to_proc
18
24
  end
19
25
  end
26
+
27
+ def async_execution?
28
+ defined?(ActiveJob) && Adhoq.config.async_execution
29
+ end
20
30
  end
21
31
  end
data/lib/adhoq/engine.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  require 'font-awesome-sass'
3
3
  require 'jquery-rails'
4
4
  require 'slim-rails'
5
+ require 'active_decorator'
5
6
 
6
7
  module Adhoq
7
8
  class Engine < ::Rails::Engine
@@ -1,42 +1,18 @@
1
1
  module Adhoq
2
2
  class Executor
3
- class << self
4
- def select(query)
5
- with_sandbox do
6
- current_connection.exec_query(query)
7
- end
8
- end
9
-
10
- def explain(query)
11
- with_sandbox do
12
- current_connection.explain(query)
13
- end
14
- end
15
-
16
- def current_connection
17
- ActiveRecord::Base.connection
18
- end
19
-
20
- def with_sandbox
21
- result = nil
22
- ActiveRecord::Base.transaction do
23
- result = yield
24
- raise ActiveRecord::Rollback
25
- end
26
- result
27
- end
28
- end
3
+ autoload 'ConnectionWrapper', 'adhoq/executor/connection_wrapper'
29
4
 
30
5
  def initialize(query)
6
+ @connection = ConnectionWrapper.new
31
7
  @query = query
32
8
  end
33
9
 
34
10
  def execute
35
- wrap_result(self.class.select(@query))
11
+ wrap_result(@connection.select(@query))
36
12
  end
37
13
 
38
14
  def explain
39
- self.class.explain(@query)
15
+ @connection.explain(@query)
40
16
  end
41
17
 
42
18
  private
@@ -0,0 +1,32 @@
1
+ module Adhoq
2
+ class Executor
3
+ class ConnectionWrapper
4
+ attr_reader :connection
5
+
6
+ def initialize
7
+ @connection = Adhoq.config.callablize(:database_connection).call
8
+ end
9
+
10
+ def select(query)
11
+ with_sandbox do
12
+ connection.exec_query(query)
13
+ end
14
+ end
15
+
16
+ def explain(query)
17
+ with_sandbox do
18
+ connection.explain(query)
19
+ end
20
+ end
21
+
22
+ def with_sandbox
23
+ result = nil
24
+ connection.transaction do
25
+ result = yield
26
+ raise ActiveRecord::Rollback
27
+ end
28
+ result
29
+ end
30
+ end
31
+ end
32
+ end
@@ -27,6 +27,7 @@ module Adhoq
27
27
  case type
28
28
  when :local_file then Adhoq::Storage::LocalFile
29
29
  when :s3 then Adhoq::Storage::S3
30
+ when :on_the_fly then Adhoq::Storage::OnTheFly
30
31
  else
31
32
  raise NotImplementedError
32
33
  end
data/lib/adhoq/storage.rb CHANGED
@@ -3,6 +3,7 @@ module Adhoq
3
3
  autoload 'FogStorage', 'adhoq/storage/fog_storage'
4
4
  autoload 'LocalFile', 'adhoq/storage/local_file'
5
5
  autoload 'S3', 'adhoq/storage/s3'
6
+ autoload 'OnTheFly', 'adhoq/storage/on_the_fly'
6
7
 
7
8
  def with_new_identifier(suffix = nil, seed = Time.now)
8
9
  dirname, fname_seed = ['%Y-%m-%d', '%H%M%S.%L'].map {|f| seed.strftime(f) }
@@ -10,6 +10,10 @@ module Adhoq
10
10
  end
11
11
  end
12
12
 
13
+ def direct_download?
14
+ false
15
+ end
16
+
13
17
  def get(identifier)
14
18
  get_raw(identifier).body
15
19
  end
@@ -0,0 +1,32 @@
1
+ module Adhoq
2
+ module Storage
3
+ class OnTheFly
4
+ PREFIX = 'memory://adhoq-on-the-fly'
5
+
6
+ attr_reader :identifier, :reports
7
+
8
+ def initialize(id_base = Process.pid)
9
+ @identifier = "#{PREFIX}-#{id_base}"
10
+ @reports = {}
11
+ end
12
+
13
+ def store(suffix = nil, seed = Time.now, &block)
14
+ Adhoq::Storage.with_new_identifier(suffix, seed) do |identifier|
15
+ @reports[identifier] = yield.tap(&:rewind)
16
+ end
17
+ end
18
+
19
+ def direct_download?
20
+ false
21
+ end
22
+
23
+ def get(identifier)
24
+ if item = @reports.delete(identifier)
25
+ item.read.tap { item.close }
26
+ else
27
+ nil
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -4,14 +4,27 @@ module Adhoq
4
4
  module Storage
5
5
  class S3 < FogStorage
6
6
  def initialize(bucket, s3_options = {})
7
- @bucket = bucket
8
- @s3 = Fog::Storage.new({provider: 'AWS'}.merge(s3_options))
7
+ @bucket = bucket
8
+ @direct_download = s3_options.delete(:direct_download)
9
+ @direct_download_options = s3_options.delete(:direct_download_options) || default_direct_download_options
10
+ @s3 = Fog::Storage.new({provider: 'AWS'}.merge(s3_options))
11
+ end
12
+
13
+ def direct_download?
14
+ @direct_download
9
15
  end
10
16
 
11
17
  def identifier
12
18
  "s3://#{@bucket}"
13
19
  end
14
20
 
21
+ def get_url(report)
22
+ get_raw(report.identifier).url(
23
+ 1.minutes.from_now.to_i,
24
+ @direct_download_options.call(report)
25
+ )
26
+ end
27
+
15
28
  private
16
29
 
17
30
  def directory
@@ -19,6 +32,17 @@ module Adhoq
19
32
 
20
33
  @directory = @s3.directories.get(@bucket) || @s3.directories.create(key: @bucket, public: false)
21
34
  end
35
+
36
+ def default_direct_download_options
37
+ proc do |report|
38
+ {
39
+ query: {
40
+ 'response-content-disposition' => "attachment; filename*=UTF-8''#{URI.encode_www_form_component(report.name)}",
41
+ 'response-content-type' => report.mime_type,
42
+ }
43
+ }
44
+ end
45
+ end
22
46
  end
23
47
  end
24
48
  end
data/lib/adhoq/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Adhoq
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.6"
3
3
  end
@@ -0,0 +1,16 @@
1
+ module Adhoq
2
+ RSpec.describe Executor::ConnectionWrapper, type: :model do
3
+ describe '.select' do
4
+ specify 'Do not reflect write access' do
5
+ expect {
6
+ Executor::ConnectionWrapper.new.select(<<-INSERT_SQL.strip_heredoc)
7
+ INSERT INTO "adhoq_queries"
8
+ ("description", "name", "query", "updated_at", "created_at")
9
+ VALUES
10
+ ("description", "name", "SELECT 1", "NOW", "NOW")
11
+ INSERT_SQL
12
+ }.not_to change(Adhoq::Query, :count)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -7,18 +7,5 @@ module Adhoq
7
7
 
8
8
  specify { expect(executor.execute).to eq Adhoq::Result.new(%w[answer], [[42]]) }
9
9
  end
10
-
11
- describe '.select' do
12
- specify 'Do not reflect write access' do
13
- expect {
14
- Executor.select(<<-INSERT_SQL.strip_heredoc)
15
- INSERT INTO "adhoq_queries"
16
- ("description", "name", "query", "updated_at", "created_at")
17
- VALUES
18
- ("description", "name", "SELECT 1", "NOW", "NOW")
19
- INSERT_SQL
20
- }.not_to change(Adhoq::Query, :count)
21
- end
22
- end
23
10
  end
24
11
  end
@@ -25,5 +25,19 @@ module Adhoq
25
25
 
26
26
  specify { expect(storage.get(identifier)).to eq "Hello adhoq!\n" }
27
27
  end
28
+
29
+ describe Storage::OnTheFly do
30
+ let(:storage) { Storage::OnTheFly.new }
31
+
32
+ let!(:identifier) do
33
+ storage.store('.txt') { StringIO.new("Hello adhoq!\n") }
34
+ end
35
+
36
+ specify { expect(storage.get(identifier)).to eq "Hello adhoq!\n" }
37
+
38
+ specify do
39
+ expect { storage.get(identifier) }.to change { storage.reports.size }.by(-1)
40
+ end
41
+ end
28
42
  end
29
43
  end
@@ -1,4 +1,25 @@
1
1
  module Adhoq
2
2
  RSpec.describe Execution, :type => :model do
3
+ before do
4
+ storage = Adhoq::Storage::OnTheFly.new
5
+ allow(Adhoq).to receive(:current_storage) { storage }
6
+ end
7
+
8
+ let(:execution) do
9
+ query = create(:adhoq_query, query: 'SELECT name, description FROM adhoq_queries')
10
+ query.execute!('xlsx')
11
+ end
12
+
13
+ specify { expect(execution.report).to be_on_the_fly }
14
+
15
+ specify 'can get report only on execution' do
16
+ expect(execution.report.data).to have_values_in_xlsx_sheet([
17
+ ["name", "description"],
18
+ ["A query", "Simple simple SELECT"]
19
+ ])
20
+
21
+ # Accessable only once
22
+ expect(execution.report.data).to be_nil
23
+ end
3
24
  end
4
25
  end
@@ -0,0 +1,26 @@
1
+ RSpec.configure do |config|
2
+ config.around(:each, async_execution: true) do |ex|
3
+ current_async_execution = Adhoq.config.async_execution
4
+
5
+ Adhoq.config.async_execution = true
6
+
7
+ ex.call
8
+
9
+ Adhoq.config.async_execution = current_async_execution
10
+ end
11
+
12
+ config.around(:each, active_job_test_adapter: true) do |ex|
13
+ current_active_job_queue_adapter = Adhoq::Engine.config.active_job.queue_adapter
14
+ current_execute_job_queue_adapter = Adhoq::ExecuteJob.queue_adapter
15
+
16
+ Adhoq::Engine.config.active_job.queue_adapter = :test
17
+ Adhoq::ExecuteJob.queue_adapter = ActiveJob::QueueAdapters::TestAdapter.new
18
+ Adhoq::ExecuteJob.queue_adapter.perform_enqueued_jobs = true
19
+
20
+ ex.call
21
+
22
+ Adhoq::ExecuteJob.queue_adapter.performed_jobs.clear
23
+ Adhoq::Engine.config.active_job.queue_adapter = current_active_job_queue_adapter
24
+ Adhoq::ExecuteJob.queue_adapter = current_execute_job_queue_adapter
25
+ end
26
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: adhoq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kyosuke MOROHASHI
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-08 00:00:00.000000000 Z
11
+ date: 2015-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -122,6 +122,34 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: active_decorator
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rouge
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: capybara
127
155
  requirement: !ruby/object:Gem::Requirement
@@ -372,19 +400,24 @@ files:
372
400
  - app/controllers/adhoq/explains_controller.rb
373
401
  - app/controllers/adhoq/previews_controller.rb
374
402
  - app/controllers/adhoq/queries_controller.rb
403
+ - app/decorators/adhoq/execution_decorator.rb
404
+ - app/decorators/adhoq/query_decorator.rb
375
405
  - app/helpers/adhoq/application_helper.rb
406
+ - app/jobs/adhoq/execute_job.rb
376
407
  - app/models/adhoq/execution.rb
377
408
  - app/models/adhoq/query.rb
378
409
  - app/models/adhoq/report.rb
379
410
  - app/models/adhoq/time_based_orders.rb
380
411
  - app/views/adhoq/application/_global_nav.html.slim
381
- - app/views/adhoq/application/_sidebar_queries_index.html.slim
382
412
  - app/views/adhoq/current_tables/index.html.slim
383
413
  - app/views/adhoq/explains/create.html.slim
384
414
  - app/views/adhoq/explains/statement_invalid.html.slim
385
415
  - app/views/adhoq/previews/create.html.slim
386
416
  - app/views/adhoq/previews/statement_invalid.html.slim
417
+ - app/views/adhoq/queries/_current_tables_leftbar.html.slim
418
+ - app/views/adhoq/queries/_execution.html.slim
387
419
  - app/views/adhoq/queries/_form.html.slim
420
+ - app/views/adhoq/queries/_queries.html.slim
388
421
  - app/views/adhoq/queries/_query.html.slim
389
422
  - app/views/adhoq/queries/edit.html.slim
390
423
  - app/views/adhoq/queries/index.html.slim
@@ -401,6 +434,7 @@ files:
401
434
  - lib/adhoq/engine.rb
402
435
  - lib/adhoq/error.rb
403
436
  - lib/adhoq/executor.rb
437
+ - lib/adhoq/executor/connection_wrapper.rb
404
438
  - lib/adhoq/global_variable.rb
405
439
  - lib/adhoq/reporter.rb
406
440
  - lib/adhoq/reporter/csv.rb
@@ -410,9 +444,11 @@ files:
410
444
  - lib/adhoq/storage.rb
411
445
  - lib/adhoq/storage/fog_storage.rb
412
446
  - lib/adhoq/storage/local_file.rb
447
+ - lib/adhoq/storage/on_the_fly.rb
413
448
  - lib/adhoq/storage/s3.rb
414
449
  - lib/adhoq/version.rb
415
450
  - lib/tasks/adhoq_tasks.rake
451
+ - spec/adhoq/executor/connection_wrapper_spec.rb
416
452
  - spec/adhoq/executor_spec.rb
417
453
  - spec/adhoq/global_variable_spec.rb
418
454
  - spec/adhoq/reporter/csv_spec.rb
@@ -424,6 +460,7 @@ files:
424
460
  - spec/models/adhoq/query_spec.rb
425
461
  - spec/models/adhoq/report_spec.rb
426
462
  - spec/spec_helper.rb
463
+ - spec/support/activejob_helper.rb
427
464
  - spec/support/codeclimate_reporter.rb
428
465
  - spec/support/feature_spec_helper.rb
429
466
  - spec/support/have_values_in_xlsx_sheet_matcher.rb
@@ -447,11 +484,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
447
484
  version: '0'
448
485
  requirements: []
449
486
  rubyforge_project:
450
- rubygems_version: 2.2.2
487
+ rubygems_version: 2.4.5
451
488
  signing_key:
452
489
  specification_version: 4
453
490
  summary: DB management console in the wild.
454
491
  test_files:
492
+ - spec/adhoq/executor/connection_wrapper_spec.rb
455
493
  - spec/adhoq/executor_spec.rb
456
494
  - spec/adhoq/global_variable_spec.rb
457
495
  - spec/adhoq/reporter/csv_spec.rb
@@ -462,6 +500,7 @@ test_files:
462
500
  - spec/models/adhoq/execution_spec.rb
463
501
  - spec/models/adhoq/query_spec.rb
464
502
  - spec/models/adhoq/report_spec.rb
503
+ - spec/support/activejob_helper.rb
465
504
  - spec/support/codeclimate_reporter.rb
466
505
  - spec/support/feature_spec_helper.rb
467
506
  - spec/support/have_values_in_xlsx_sheet_matcher.rb
@@ -1,10 +0,0 @@
1
- h2
2
- | Queries
3
- = link_to 'Create new', :root, class: 'btn btn-primary new-query'
4
- ul.queries.list-unstyled
5
- - Adhoq::Query.recent_first.each do |query|
6
- li.panel.panel-default[query]
7
- .panel-heading
8
- h2= link_to query.name, query_path(query)
9
- p.panel-body.description= query.description
10
-