superset 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.buildkite/pipeline.yml +16 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +48 -0
- data/Dockerfile +17 -0
- data/LICENSE +21 -0
- data/README.md +205 -0
- data/Rakefile +12 -0
- data/doc/duplicate_dashboards.md +214 -0
- data/doc/setting_up_personal_api_credentials.md +127 -0
- data/docker-compose.override.yml +10 -0
- data/docker-compose.yml +8 -0
- data/env.sample +9 -0
- data/lib/loggers/duplicate_dashboard_logger.rb +15 -0
- data/lib/superset/authenticator.rb +55 -0
- data/lib/superset/chart/bulk_delete.rb +40 -0
- data/lib/superset/chart/delete.rb +30 -0
- data/lib/superset/chart/get.rb +56 -0
- data/lib/superset/chart/list.rb +59 -0
- data/lib/superset/chart/update_dataset.rb +90 -0
- data/lib/superset/client.rb +53 -0
- data/lib/superset/credential/api_user.rb +25 -0
- data/lib/superset/credential/embedded_user.rb +25 -0
- data/lib/superset/dashboard/bulk_delete.rb +42 -0
- data/lib/superset/dashboard/bulk_delete_cascade.rb +52 -0
- data/lib/superset/dashboard/charts/list.rb +47 -0
- data/lib/superset/dashboard/compare.rb +94 -0
- data/lib/superset/dashboard/copy.rb +78 -0
- data/lib/superset/dashboard/datasets/list.rb +74 -0
- data/lib/superset/dashboard/delete.rb +42 -0
- data/lib/superset/dashboard/embedded/get.rb +56 -0
- data/lib/superset/dashboard/embedded/put.rb +35 -0
- data/lib/superset/dashboard/export.rb +98 -0
- data/lib/superset/dashboard/get.rb +51 -0
- data/lib/superset/dashboard/info.rb +17 -0
- data/lib/superset/dashboard/list.rb +99 -0
- data/lib/superset/dashboard/put.rb +37 -0
- data/lib/superset/dashboard/warm_up_cache.rb +42 -0
- data/lib/superset/database/get.rb +30 -0
- data/lib/superset/database/get_schemas.rb +25 -0
- data/lib/superset/database/list.rb +51 -0
- data/lib/superset/dataset/bulk_delete.rb +41 -0
- data/lib/superset/dataset/create.rb +62 -0
- data/lib/superset/dataset/delete.rb +30 -0
- data/lib/superset/dataset/duplicate.rb +62 -0
- data/lib/superset/dataset/get.rb +56 -0
- data/lib/superset/dataset/list.rb +41 -0
- data/lib/superset/dataset/update_query.rb +56 -0
- data/lib/superset/dataset/update_schema.rb +120 -0
- data/lib/superset/dataset/warm_up_cache.rb +41 -0
- data/lib/superset/display.rb +42 -0
- data/lib/superset/enumerations/object_type.rb +11 -0
- data/lib/superset/file_utilities.rb +19 -0
- data/lib/superset/guest_token.rb +69 -0
- data/lib/superset/logger.rb +20 -0
- data/lib/superset/request.rb +62 -0
- data/lib/superset/route_info.rb +34 -0
- data/lib/superset/security/permissions_resources/list.rb +22 -0
- data/lib/superset/security/role/create.rb +25 -0
- data/lib/superset/security/role/get.rb +32 -0
- data/lib/superset/security/role/list.rb +45 -0
- data/lib/superset/security/role/permission/create.rb +35 -0
- data/lib/superset/security/role/permission/get.rb +37 -0
- data/lib/superset/security/user/create.rb +49 -0
- data/lib/superset/security/user/get.rb +27 -0
- data/lib/superset/security/user/list.rb +42 -0
- data/lib/superset/services/duplicate_dashboard.rb +298 -0
- data/lib/superset/sqllab/execute.rb +52 -0
- data/lib/superset/tag/add_to_object.rb +46 -0
- data/lib/superset/tag/get.rb +30 -0
- data/lib/superset/tag/list.rb +37 -0
- data/lib/superset/version.rb +5 -0
- data/lib/superset.rb +17 -0
- data/log/README.md +4 -0
- data/superset.gemspec +55 -0
- metadata +300 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# WARNING: DESTRUCTIVE OPERATION .. use with caution
|
4
|
+
# This class is used to delete multiple dashboards and all related charts and datasets.
|
5
|
+
# There are NO CHECKS currently to confirm if a dataset is used on other dashboards.
|
6
|
+
|
7
|
+
module Superset
|
8
|
+
module Dashboard
|
9
|
+
class BulkDeleteCascade
|
10
|
+
class InvalidParameterError < StandardError; end
|
11
|
+
|
12
|
+
attr_reader :dashboard_ids
|
13
|
+
|
14
|
+
def initialize(dashboard_ids: [])
|
15
|
+
@dashboard_ids = dashboard_ids
|
16
|
+
end
|
17
|
+
|
18
|
+
def perform
|
19
|
+
raise InvalidParameterError, "dashboard_ids array of integers expected" unless dashboard_ids.is_a?(Array)
|
20
|
+
raise InvalidParameterError, "dashboard_ids array must contain Integer only values" unless dashboard_ids.all? { |item| item.is_a?(Integer) }
|
21
|
+
|
22
|
+
dashboard_ids.sort.each do |dashboard_id|
|
23
|
+
logger.info("Dashboard Id: #{dashboard_id.to_s} Attempting CASCADE delete of dashboard, charts, datasets")
|
24
|
+
delete_datasets(dashboard_id)
|
25
|
+
delete_charts(dashboard_id)
|
26
|
+
delete_dashboard(dashboard_id)
|
27
|
+
end
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def delete_datasets(dashboard_id)
|
34
|
+
datasets_to_delete = Superset::Dashboard::Datasets::List.new(dashboard_id).datasets_details.map{|d| d[:id] }
|
35
|
+
Superset::Dataset::BulkDelete.new(dataset_ids: datasets_to_delete).perform if datasets_to_delete.any?
|
36
|
+
end
|
37
|
+
|
38
|
+
def delete_charts(dashboard_id)
|
39
|
+
charts_to_delete = Superset::Dashboard::Charts::List.new(dashboard_id).chart_ids
|
40
|
+
Superset::Chart::BulkDelete.new(chart_ids: charts_to_delete).perform if charts_to_delete.any?
|
41
|
+
end
|
42
|
+
|
43
|
+
def delete_dashboard(dashboard_id)
|
44
|
+
Superset::Dashboard::Delete.new(dashboard_id: dashboard_id, confirm_zero_charts: true).perform
|
45
|
+
end
|
46
|
+
|
47
|
+
def logger
|
48
|
+
@logger ||= Superset::Logger.new
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Superset
|
2
|
+
module Dashboard
|
3
|
+
module Charts
|
4
|
+
class List < Superset::Request
|
5
|
+
attr_reader :id # dashboard id
|
6
|
+
|
7
|
+
def self.call(id)
|
8
|
+
self.new(id).list
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(id)
|
12
|
+
@id = id
|
13
|
+
end
|
14
|
+
|
15
|
+
def chart_ids
|
16
|
+
result.map { |c| c[:id] }
|
17
|
+
end
|
18
|
+
|
19
|
+
def rows
|
20
|
+
result.map do |c|
|
21
|
+
[
|
22
|
+
c[:id],
|
23
|
+
c[:slice_name],
|
24
|
+
c[:form_data][:datasource],
|
25
|
+
# c[:form_data][:dashboards] # NOTE: form_data dashboards is not accurate .. looks to be bugs related to copying charts
|
26
|
+
]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def route
|
33
|
+
"dashboard/#{id}/charts"
|
34
|
+
end
|
35
|
+
|
36
|
+
def list_attributes
|
37
|
+
['id', 'slice_name', 'datasource'].map(&:to_sym)
|
38
|
+
end
|
39
|
+
|
40
|
+
# when displaying a list of datasets, show dashboard id and title as well
|
41
|
+
def title
|
42
|
+
@title ||= [id, Superset::Dashboard::Get.new(id).title].join(' ')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# A validation checker for comparing dashboards.
|
2
|
+
# This class is used to compare two dashboards by their datasets, charts, native filters and cross filters.
|
3
|
+
# Output is displayed in a table format to the ruby console
|
4
|
+
#
|
5
|
+
# Usage: Superset::Dashboard::Compare.new(first_dashboard_id: 322, second_dashboard_id: 347).perform
|
6
|
+
#
|
7
|
+
module Superset
|
8
|
+
module Dashboard
|
9
|
+
class Compare
|
10
|
+
|
11
|
+
attr_reader :first_dashboard_id, :second_dashboard_id
|
12
|
+
|
13
|
+
def initialize(first_dashboard_id: , second_dashboard_id: )
|
14
|
+
@first_dashboard_id = first_dashboard_id
|
15
|
+
@second_dashboard_id = second_dashboard_id
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def perform
|
20
|
+
raise "Error: first_dashboard_id integer is required" unless first_dashboard_id.present? && first_dashboard_id.is_a?(Integer)
|
21
|
+
raise "Error: second_dashboard_id integer is required" unless second_dashboard_id.present? && second_dashboard_id.is_a?(Integer)
|
22
|
+
|
23
|
+
list_datasets
|
24
|
+
list_charts
|
25
|
+
list_native_filters
|
26
|
+
list_cross_filters
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def first_dashboard
|
31
|
+
@first_dashboard ||= Get.new(first_dashboard_id).result
|
32
|
+
end
|
33
|
+
|
34
|
+
def second_dashboard
|
35
|
+
@second_dashboard ||= Get.new(second_dashboard_id).result
|
36
|
+
end
|
37
|
+
|
38
|
+
def list_datasets
|
39
|
+
puts "\n ====== DASHBOARD DATASETS ====== "
|
40
|
+
Superset::Dashboard::Datasets::List.new(first_dashboard_id).list
|
41
|
+
Superset::Dashboard::Datasets::List.new(second_dashboard_id).list
|
42
|
+
end
|
43
|
+
|
44
|
+
def list_charts
|
45
|
+
puts "\n ====== DASHBOARD CHARTS ====== "
|
46
|
+
Superset::Dashboard::Charts::List.new(first_dashboard_id).list
|
47
|
+
puts ''
|
48
|
+
Superset::Dashboard::Charts::List.new(second_dashboard_id).list
|
49
|
+
end
|
50
|
+
|
51
|
+
def list_native_filters
|
52
|
+
puts "\n ====== DASHBOARD NATIVE FILTERS ====== "
|
53
|
+
list_native_filters_for(first_dashboard)
|
54
|
+
puts ''
|
55
|
+
list_native_filters_for(second_dashboard)
|
56
|
+
end
|
57
|
+
|
58
|
+
def list_cross_filters
|
59
|
+
puts "\n ====== DASHBOARD CROSS FILTERS ====== "
|
60
|
+
list_cross_filters_for(first_dashboard)
|
61
|
+
puts ''
|
62
|
+
list_cross_filters_for(second_dashboard)
|
63
|
+
end
|
64
|
+
|
65
|
+
def native_filter_configuration(dashboard_result)
|
66
|
+
rows = []
|
67
|
+
JSON.parse(dashboard_result['json_metadata'])['native_filter_configuration'].each do |filter|
|
68
|
+
filter['targets'].each {|t| rows << [ t['column']['name'], t['datasetId'] ] }
|
69
|
+
end
|
70
|
+
rows
|
71
|
+
end
|
72
|
+
|
73
|
+
def list_native_filters_for(dashboard_result)
|
74
|
+
puts Terminal::Table.new(
|
75
|
+
title: [dashboard_result['id'], dashboard_result['dashboard_title']].join(' - '),
|
76
|
+
headings: ['Filter Name', 'Dataset Id'],
|
77
|
+
rows: native_filter_configuration(dashboard_result)
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def cross_filter_configuration(dashboard_result)
|
82
|
+
JSON.parse(dashboard_result['json_metadata'])['chart_configuration'].map {|k, v| [ v['id'], v['crossFilters'].to_s ] }
|
83
|
+
end
|
84
|
+
|
85
|
+
def list_cross_filters_for(dashboard_result)
|
86
|
+
puts Terminal::Table.new(
|
87
|
+
title: [dashboard_result['id'], dashboard_result['dashboard_title']].join(' - '),
|
88
|
+
headings: ['Chart Id', 'Cross Filter Config'],
|
89
|
+
rows: cross_filter_configuration(dashboard_result)
|
90
|
+
)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
|
2
|
+
# TODO: some work to happen around TAGS still .. ie 'template' tag would indicate it tested and available to be copied.
|
3
|
+
# TODO: also need to ensoure that the embedded details are not duplicated across to the new dashboard
|
4
|
+
|
5
|
+
module Superset
|
6
|
+
module Dashboard
|
7
|
+
class Copy < Superset::Request
|
8
|
+
|
9
|
+
attr_reader :source_dashboard_id, :duplicate_slices, :clear_shared_label_colors
|
10
|
+
|
11
|
+
def initialize(source_dashboard_id: , duplicate_slices: false, clear_shared_label_colors: false)
|
12
|
+
@source_dashboard_id = source_dashboard_id
|
13
|
+
@duplicate_slices = duplicate_slices # boolean indicates whether to duplicate charts OR keep the new dashboard pointing to the same charts as the original
|
14
|
+
@clear_shared_label_colors = clear_shared_label_colors
|
15
|
+
end
|
16
|
+
|
17
|
+
def perform
|
18
|
+
raise "Error: source_dashboard_id integer is required" unless source_dashboard_id.present? && source_dashboard_id.is_a?(Integer)
|
19
|
+
raise "Error: duplicate_slices must be a boolean" unless duplicate_slices_is_boolean?
|
20
|
+
|
21
|
+
adjust_json_metadata
|
22
|
+
response
|
23
|
+
Superset::Dashboard::Get.new(id).perform # return the full new dashboard object
|
24
|
+
end
|
25
|
+
|
26
|
+
def params
|
27
|
+
{
|
28
|
+
"css" => "{}",
|
29
|
+
"dashboard_title" => "#{source_dashboard.title}",
|
30
|
+
"duplicate_slices" => duplicate_slices,
|
31
|
+
"json_metadata" => new_dashboard_json_metadata.to_json,
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def response
|
36
|
+
@response ||= client.post(route, params)
|
37
|
+
end
|
38
|
+
|
39
|
+
def id
|
40
|
+
response["result"]["id"]
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def route
|
46
|
+
"dashboard/#{source_dashboard_id}/copy/"
|
47
|
+
end
|
48
|
+
|
49
|
+
def adjust_json_metadata
|
50
|
+
# when copying a DB via the API, chart positions need to be nested under json_metadata according to the GUI copy function (as per dev tools investigation in browser)
|
51
|
+
new_dashboard_json_metadata.merge!( "positions" => source_dashboard.positions )
|
52
|
+
|
53
|
+
if clear_shared_label_colors
|
54
|
+
# if coping a dashboard to a new db schema .. shared label colors will not be relevant/match as they are specific to the previous schemas dataset values
|
55
|
+
new_dashboard_json_metadata.merge!( "shared_label_colors" => {} )
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def source_dashboard
|
60
|
+
@source_dashboard ||= begin
|
61
|
+
dash = Get.new(source_dashboard_id)
|
62
|
+
dash.response
|
63
|
+
dash
|
64
|
+
rescue => e
|
65
|
+
raise "Error retrieving source dashboard #{e.message}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def new_dashboard_json_metadata
|
70
|
+
@new_dashboard_json_metadata ||= source_dashboard.json_metadata
|
71
|
+
end
|
72
|
+
|
73
|
+
def duplicate_slices_is_boolean?
|
74
|
+
[true, false].include?(duplicate_slices)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# WARNING: Does not take into account datasets with queries that have embedded schema references.
|
2
|
+
# ie " select * from schema1.table join schema2.table" for a dataset query will ONLY return the datasets config schema setting not the sql query schema references which refers to 2 distinct schemas.
|
3
|
+
#
|
4
|
+
# WARNING: Does not return Filter Datasets for the dashboard
|
5
|
+
|
6
|
+
module Superset
|
7
|
+
module Dashboard
|
8
|
+
module Datasets
|
9
|
+
class List < Superset::Request
|
10
|
+
attr_reader :id # dashboard id
|
11
|
+
|
12
|
+
def self.call(id)
|
13
|
+
self.new(id).list
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(id)
|
17
|
+
@id = id
|
18
|
+
end
|
19
|
+
|
20
|
+
def perform
|
21
|
+
response
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def schemas
|
26
|
+
@schemas ||= begin
|
27
|
+
all_dashboard_schemas = result.map {|d| d[:schema] }.uniq
|
28
|
+
|
29
|
+
# For the current superset setup we will assume a dashboard datasets will point to EXACTLY one schema, their own.
|
30
|
+
# if not .. we need to know about it. Potentially we could override this check if others do not consider it a problem.
|
31
|
+
if all_dashboard_schemas.count > 1
|
32
|
+
Rollbar.error("SUPERSET DASHBOARD ERROR: Dashboard id #{id} has multiple dataset schema linked: #{all_dashboard_schemas.to_s}")
|
33
|
+
end
|
34
|
+
all_dashboard_schemas
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def datasets_details
|
39
|
+
result.map do |details|
|
40
|
+
details.slice('id', 'datasource_name', 'schema', 'sql').merge('database' => details['database'].slice('id', 'name', 'backend')).with_indifferent_access
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def route
|
47
|
+
"dashboard/#{id}/datasets"
|
48
|
+
end
|
49
|
+
|
50
|
+
def list_attributes
|
51
|
+
['id', 'datasource_name', 'database_id', 'database_name', 'database_backend', 'schema'].map(&:to_sym)
|
52
|
+
end
|
53
|
+
|
54
|
+
def rows
|
55
|
+
result.map do |d|
|
56
|
+
[
|
57
|
+
d[:id],
|
58
|
+
d[:datasource_name],
|
59
|
+
d[:database][:id],
|
60
|
+
d[:database][:name],
|
61
|
+
d[:database][:backend],
|
62
|
+
d[:schema]
|
63
|
+
]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# when displaying a list of datasets, show dashboard title as well
|
68
|
+
def title
|
69
|
+
@title ||= [id, Superset::Dashboard::Get.new(id).title].join(' ')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Superset
|
4
|
+
module Dashboard
|
5
|
+
class Delete < Superset::Request
|
6
|
+
|
7
|
+
attr_reader :dashboard_id, :confirm_zero_charts
|
8
|
+
|
9
|
+
def initialize(dashboard_id: , confirm_zero_charts: true)
|
10
|
+
@dashboard_id = dashboard_id
|
11
|
+
@confirm_zero_charts = confirm_zero_charts
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform
|
15
|
+
raise InvalidParameterError, "dashboard_id integer is required" unless dashboard_id.present? && dashboard_id.is_a?(Integer)
|
16
|
+
|
17
|
+
confirm_zero_charts_on_dashboard if confirm_zero_charts
|
18
|
+
|
19
|
+
logger.info("Attempting to delete dashboard with id: #{dashboard_id}")
|
20
|
+
response
|
21
|
+
end
|
22
|
+
|
23
|
+
def response
|
24
|
+
@response ||= client.delete(route)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def confirm_zero_charts_on_dashboard
|
30
|
+
raise "Error: Dashboard includes #{dashboard_charts.count} charts. Please delete all charts before deleting the dashboard or override and set confirm_zero_charts: false" if dashboard_charts.count.positive?
|
31
|
+
end
|
32
|
+
|
33
|
+
def dashboard_charts
|
34
|
+
@dashboard_charts ||= Superset::Dashboard::Charts::List.new(dashboard_id).rows
|
35
|
+
end
|
36
|
+
|
37
|
+
def route
|
38
|
+
"dashboard/#{dashboard_id}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Superset
|
2
|
+
module Dashboard
|
3
|
+
module Embedded
|
4
|
+
class Get < Superset::Request
|
5
|
+
attr_reader :id # dashboard id
|
6
|
+
|
7
|
+
def self.call(id)
|
8
|
+
self.new(id).list
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(id)
|
12
|
+
@id = id
|
13
|
+
end
|
14
|
+
|
15
|
+
def response
|
16
|
+
@response ||= client.get(route)
|
17
|
+
rescue Happi::Error::NotFound => e
|
18
|
+
logger.info("Dashboard #{id} has no Embedded settings. (skipping)") # some dashboards don't have embedded settings, fine to ignore.
|
19
|
+
@response = { result: [] }.with_indifferent_access
|
20
|
+
@response
|
21
|
+
end
|
22
|
+
|
23
|
+
def result
|
24
|
+
response[:result].empty? ? [] : [ super ] # wrap single result in an array so it can be used in the tt list and table
|
25
|
+
end
|
26
|
+
|
27
|
+
def allowed_domains
|
28
|
+
result.first['allowed_domains'] unless response[:result].empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def uuid
|
32
|
+
result.first['uuid'] unless response[:result].empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def list
|
36
|
+
super unless response[:result].empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def route
|
42
|
+
"dashboard/#{id}/embedded"
|
43
|
+
end
|
44
|
+
|
45
|
+
def list_attributes
|
46
|
+
[:dashboard_id, :uuid, :allowed_domains, :changed_on]
|
47
|
+
end
|
48
|
+
|
49
|
+
# when displaying embedded details, show dashboard title as well
|
50
|
+
def title
|
51
|
+
Superset::Dashboard::Get.new(id).title
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Superset
|
2
|
+
module Dashboard
|
3
|
+
module Embedded
|
4
|
+
class Put < Superset::Request
|
5
|
+
attr_reader :dashboard_id, :allowed_domains
|
6
|
+
|
7
|
+
def initialize(dashboard_id: , allowed_domains: )
|
8
|
+
@dashboard_id = dashboard_id
|
9
|
+
@allowed_domains = allowed_domains
|
10
|
+
end
|
11
|
+
|
12
|
+
def response
|
13
|
+
raise InvalidParameterError, 'dashboard_id integer is required' if dashboard_id.nil? || dashboard_id.class != Integer
|
14
|
+
raise InvalidParameterError, 'allowed_domains array is required' if allowed_domains.nil? || allowed_domains.class != Array
|
15
|
+
|
16
|
+
@response ||= client.put(route, params)
|
17
|
+
end
|
18
|
+
|
19
|
+
def params
|
20
|
+
{ "allowed_domains": allowed_domains }
|
21
|
+
end
|
22
|
+
|
23
|
+
def uuid
|
24
|
+
result['uuid'] unless response[:result].empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def route
|
30
|
+
"dashboard/#{dashboard_id}/embedded"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# Will export the zip file to /tmp/superset_dashboards with zip filename adjusted to include the dashboard_id
|
2
|
+
# Example zipfile: dashboard_#{dashboard_id}_export_#{datestamp}.zip
|
3
|
+
# Will then unzip and copy the files into the destination_path with the dashboard_id as a subfolder
|
4
|
+
#
|
5
|
+
# Usage
|
6
|
+
# Superset::Dashboard::Export.new(dashboard_id: 15, destination_path: '/tmp/superset_dashboard_backups/').perform
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'superset/file_utilities'
|
10
|
+
|
11
|
+
module Superset
|
12
|
+
module Dashboard
|
13
|
+
class Export < Request
|
14
|
+
include FileUtilities
|
15
|
+
|
16
|
+
TMP_SUPERSET_DASHBOARD_PATH = '/tmp/superset_dashboards'
|
17
|
+
|
18
|
+
attr_reader :dashboard_id, :destination_path
|
19
|
+
|
20
|
+
def initialize(dashboard_id: , destination_path: )
|
21
|
+
@dashboard_id = dashboard_id
|
22
|
+
@destination_path = destination_path.chomp('/')
|
23
|
+
end
|
24
|
+
|
25
|
+
def perform
|
26
|
+
create_tmp_dir
|
27
|
+
save_exported_zip_file
|
28
|
+
unzip_files
|
29
|
+
copy_export_files_to_destination_path if destination_path
|
30
|
+
end
|
31
|
+
|
32
|
+
def response
|
33
|
+
@response ||= client.call(
|
34
|
+
:get,
|
35
|
+
client.url(route),
|
36
|
+
client.param_check(params)
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def params
|
43
|
+
{ "q": "!(#{dashboard_id})" } # pulled off chrome dev tools doing a GUI export. Swagger interface not helpfull with this endpoint.
|
44
|
+
end
|
45
|
+
|
46
|
+
def save_exported_zip_file
|
47
|
+
File.open(zip_file_name, 'wb') { |fp| fp.write(response.body) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def unzip_files
|
51
|
+
@extracted_files = unzip_file(zip_file_name, tmp_uniq_dashboard_path)
|
52
|
+
end
|
53
|
+
|
54
|
+
def download_folder
|
55
|
+
File.dirname(extracted_files[0])
|
56
|
+
end
|
57
|
+
|
58
|
+
def copy_export_files_to_destination_path
|
59
|
+
path_with_dash_id = File.join(destination_path, dashboard_id.to_s)
|
60
|
+
FileUtils.mkdir_p(path_with_dash_id) unless File.directory?(path_with_dash_id)
|
61
|
+
|
62
|
+
Dir.glob("#{download_folder}/*").each do |item|
|
63
|
+
FileUtils.cp_r(item, path_with_dash_id)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def zip_file_name
|
68
|
+
@zip_file_name ||= "#{tmp_uniq_dashboard_path}/dashboard_#{dashboard_id}_export_#{datestamp}.zip"
|
69
|
+
end
|
70
|
+
|
71
|
+
def create_tmp_dir
|
72
|
+
FileUtils.mkdir_p(tmp_uniq_dashboard_path) unless File.directory?(tmp_uniq_dashboard_path)
|
73
|
+
end
|
74
|
+
|
75
|
+
# uniq random tmp folder name for each export
|
76
|
+
# this will allow us to do a wildcard glop on the folder to get the files
|
77
|
+
def tmp_uniq_dashboard_path
|
78
|
+
@tmp_uniq_dashboard_path ||= File.join(TMP_SUPERSET_DASHBOARD_PATH, uuid)
|
79
|
+
end
|
80
|
+
|
81
|
+
def uuid
|
82
|
+
SecureRandom.uuid
|
83
|
+
end
|
84
|
+
|
85
|
+
def extracted_files
|
86
|
+
@extracted_files ||= []
|
87
|
+
end
|
88
|
+
|
89
|
+
def route
|
90
|
+
"dashboard/export/"
|
91
|
+
end
|
92
|
+
|
93
|
+
def datestamp
|
94
|
+
@datestamp ||= Time.now.strftime('%Y%m%d')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Superset
|
2
|
+
module Dashboard
|
3
|
+
class Get < Superset::Request
|
4
|
+
|
5
|
+
attr_reader :id
|
6
|
+
|
7
|
+
def initialize(id)
|
8
|
+
@id = id
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.call(id)
|
12
|
+
self.new(id).list
|
13
|
+
end
|
14
|
+
|
15
|
+
def perform
|
16
|
+
response
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def title
|
21
|
+
"#{result['dashboard_title']}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def json_metadata
|
25
|
+
JSON.parse(result['json_metadata'])
|
26
|
+
end
|
27
|
+
|
28
|
+
def positions
|
29
|
+
JSON.parse(result['position_json'])
|
30
|
+
end
|
31
|
+
|
32
|
+
def url
|
33
|
+
"#{superset_host}#{result['url']}"
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def route
|
39
|
+
"dashboard/#{id}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def rows
|
43
|
+
result['charts'].map {|c| [c]}
|
44
|
+
end
|
45
|
+
|
46
|
+
def headings
|
47
|
+
['Charts']
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|