superset 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,127 @@
|
|
1
|
+
# API Credentials
|
2
|
+
|
3
|
+
Superset API Credentials are essentially the username, password and host of the Superset host users account.
|
4
|
+
|
5
|
+
If you know these already, plug them and your host value into the `.env` and it should all just work. A sample env.sample is provided as a template.
|
6
|
+
|
7
|
+
Create your own .env file with
|
8
|
+
|
9
|
+
```
|
10
|
+
cp env.sample .env
|
11
|
+
```
|
12
|
+
|
13
|
+
Adjust .env as required.
|
14
|
+
```
|
15
|
+
SUPERSET_HOST="https://your-superset-host.com"
|
16
|
+
SUPERSET_API_USERNAME="your-username"
|
17
|
+
SUPERSET_API_PASSWORD="your-password"
|
18
|
+
```
|
19
|
+
|
20
|
+
If you have multiple superset hosts across various environments you also have the option
|
21
|
+
to create individual env files per environment. More details below.
|
22
|
+
|
23
|
+
Starting a ruby console with `bin/console` will auto load the env vars.
|
24
|
+
|
25
|
+
## What is my user name?
|
26
|
+
|
27
|
+
If your Superset instance is setup to authenicate via SSO then your authenticating agent will most likely have provided a username for you in the form of a UUID value.
|
28
|
+
|
29
|
+
This is easily retrieved on you User Profile page in Superset.
|
30
|
+
|
31
|
+
Optionally use jinja template macro in sql lab.
|
32
|
+
|
33
|
+
```
|
34
|
+
select '{{ current_username() }}' as current_username;
|
35
|
+
```
|
36
|
+
|
37
|
+
## Creating / Updating your password via Swagger interface
|
38
|
+
|
39
|
+
A common setup is to use SSO to enable user access in Superset. This would mean your authenticating agent is your SSO provider service ( ie Azure ) and most likely you do not have / need a password configured for your Superset user for GUI access.
|
40
|
+
|
41
|
+
If this is the case, you will need to add your own password via hitting the superset API using the swagger interface.
|
42
|
+
|
43
|
+
Firstly you will need your superset user id, which is the superset users table PK that is assigned to you.
|
44
|
+
|
45
|
+
It appears this user id value is not exposed on the Users profile page in Superset. Depending on your level of access within your Superset instance you could:
|
46
|
+
- access the Users List page, find your user, and mouse over the edit button to reveal the Url and user id param value.
|
47
|
+
- got to sql lab and use a jinja template predefined macro to retrieve your users id.
|
48
|
+
```
|
49
|
+
select '{{ current_user_id() }}' as current_user_id;
|
50
|
+
```
|
51
|
+
- ask your superset admin to tell you what your personal superset user id is.
|
52
|
+
|
53
|
+
Once you have your user id value, open the Swagger API page on you superset host.
|
54
|
+
`https://{your-superset-host}/swagger/v1`
|
55
|
+
|
56
|
+
Scroll down to the 'Security Users' area and find the PUT request that will update your users record.
|
57
|
+
|
58
|
+
PUT `/api/v1/security/users/{pk}`
|
59
|
+
|
60
|
+
Click 'Try it Out' and add your users ID in the PK input box.
|
61
|
+
|
62
|
+
Edit the params to only consist of only the password field and the value of your new password.
|
63
|
+
|
64
|
+
```
|
65
|
+
{
|
66
|
+
"password": "{some-long-complex-random-password-value}"
|
67
|
+
}
|
68
|
+
```
|
69
|
+
|
70
|
+
And click Execute.
|
71
|
+
|
72
|
+
Within your `.env` now add your username and password.
|
73
|
+
|
74
|
+
# Accessing API across Multiple Environments
|
75
|
+
|
76
|
+
Given some local development requirements where you have to access multiple superset hosts across various environments with different credentials you can setup the env creds as follows.
|
77
|
+
|
78
|
+
Just set the overall superset environment in `.env`
|
79
|
+
|
80
|
+
```
|
81
|
+
# .env file holding one setting for the overall superset environment
|
82
|
+
SUPERSET_ENVIRONMENT='staging'
|
83
|
+
```
|
84
|
+
|
85
|
+
Then create a new file called `.env-staging` that holds your superset staging host and credentials.
|
86
|
+
|
87
|
+
```
|
88
|
+
# specific settings for the superset staging host
|
89
|
+
SUPERSET_HOST="https://your-staging-superset-host.com"
|
90
|
+
SUPERSET_API_USERNAME="staging-user-here"
|
91
|
+
SUPERSET_API_PASSWORD="set-password-here"
|
92
|
+
```
|
93
|
+
|
94
|
+
Do the same for production env.
|
95
|
+
Create a new file called `.env-production` that holds your superset production host and credentials.
|
96
|
+
|
97
|
+
```
|
98
|
+
# specific settings for the superset production host
|
99
|
+
SUPERSET_HOST="https://your-production-superset-host.com"
|
100
|
+
SUPERSET_API_USERNAME="production-user-here"
|
101
|
+
SUPERSET_API_PASSWORD="set-password-here"
|
102
|
+
```
|
103
|
+
|
104
|
+
The command `bin/console` will then load your env file depending on the value in ENV['SUPERSET_ENVIRONMENT'] from the primary `.env`.
|
105
|
+
|
106
|
+
When you need to switch envs, exit the console, edit the .env to your desired value, eg `production`, then open a console again.
|
107
|
+
|
108
|
+
Bonus is the Pry prompt will now also include the `SUPERSET_ENVIRONMENT` value.
|
109
|
+
|
110
|
+
```
|
111
|
+
bin/console
|
112
|
+
ENV configuration loaded from from .env-staging
|
113
|
+
[1] (ENV:STAGING)> Superset::Dashboard::List.new(title_contains: 'video').list
|
114
|
+
|
115
|
+
Happi: GET https://your-staging-superset-host.com/api/v1/dashboard/?q=(filters:!((col:dashboard_title,opr:ct,value:'video')),page:0,page_size:100), {}
|
116
|
+
+----+------------------+-----------+------------------------------------------------------------------+
|
117
|
+
| Superset::Dashboard::List |
|
118
|
+
+----+------------------+-----------+------------------------------------------------------------------+
|
119
|
+
| Id | Dashboard title | Status | Url |
|
120
|
+
+----+------------------+-----------+------------------------------------------------------------------+
|
121
|
+
| 6 | Video Game Sales | published | https://your-staging-superset-host.com/superset/dashboard/6/ |
|
122
|
+
+----+------------------+-----------+------------------------------------------------------------------+
|
123
|
+
```
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
|
data/docker-compose.yml
ADDED
data/env.sample
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
SUPERSET_HOST="https://your-superset-host.com"
|
2
|
+
|
3
|
+
# general api calls
|
4
|
+
SUPERSET_API_USERNAME="USERNAME"
|
5
|
+
SUPERSET_API_PASSWORD="PASSWORD"
|
6
|
+
|
7
|
+
# embedded user guest token api calls
|
8
|
+
# SUPERSET_EMBEDDED_USERNAME="EMBEDDED-USERNAME"
|
9
|
+
# SUPERSET_EMBEDDED_PASSWORD="EMBEDDED-PASSWORD"
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Superset
|
2
|
+
class Authenticator
|
3
|
+
class CredentialMissingError < StandardError; end
|
4
|
+
|
5
|
+
attr_reader :credentials
|
6
|
+
|
7
|
+
def initialize(credentials)
|
8
|
+
@credentials = credentials
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.call(credentials)
|
12
|
+
self.new(credentials).access_token
|
13
|
+
end
|
14
|
+
|
15
|
+
def access_token
|
16
|
+
response_body['access_token']
|
17
|
+
end
|
18
|
+
|
19
|
+
def refresh_token
|
20
|
+
response_body['refresh_token']
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate_credential_existance
|
24
|
+
raise CredentialMissingError, 'password not set' unless credentials[:password].present?
|
25
|
+
raise CredentialMissingError, 'username not set' unless credentials[:username].present?
|
26
|
+
end
|
27
|
+
|
28
|
+
def superset_host
|
29
|
+
raise CredentialMissingError, "SUPERSET_HOST not found" unless ENV['SUPERSET_HOST'].present?
|
30
|
+
|
31
|
+
ENV['SUPERSET_HOST']
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def response
|
37
|
+
validate_credential_existance
|
38
|
+
@response ||= connection.post(route,
|
39
|
+
credentials.to_json,
|
40
|
+
'Content-Type' => 'application/json')
|
41
|
+
end
|
42
|
+
|
43
|
+
def route
|
44
|
+
'api/v1/security/login'
|
45
|
+
end
|
46
|
+
|
47
|
+
def response_body
|
48
|
+
JSON.parse(response.env['body'] || response.env['response_body'])
|
49
|
+
end
|
50
|
+
|
51
|
+
def connection
|
52
|
+
Faraday.new(superset_host)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# TODO: the gui delete has a confirmation step, this API call does not.
|
3
|
+
# Potentially we could add a confirm_delete parameter to the constructor that would ensure that all charts either
|
4
|
+
# 1 belong to only an expected dashboard before deleting
|
5
|
+
# 2 or do not belong to any dashboards
|
6
|
+
# ( not sure if this needed at this point )
|
7
|
+
|
8
|
+
module Superset
|
9
|
+
module Chart
|
10
|
+
class BulkDelete < Superset::Request
|
11
|
+
attr_reader :chart_ids
|
12
|
+
|
13
|
+
def initialize(chart_ids: [])
|
14
|
+
@chart_ids = chart_ids
|
15
|
+
end
|
16
|
+
|
17
|
+
def perform
|
18
|
+
raise InvalidParameterError, "chart_ids array of integers expected" unless chart_ids.is_a?(Array)
|
19
|
+
raise InvalidParameterError, "chart_ids array must contain Integer only values" unless chart_ids.all? { |item| item.is_a?(Integer) }
|
20
|
+
|
21
|
+
logger.info("Attempting to delete charts with id: #{chart_ids.join(', ')}")
|
22
|
+
response
|
23
|
+
end
|
24
|
+
|
25
|
+
def response
|
26
|
+
@response ||= client.delete(route, params)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def params
|
32
|
+
{ q: "!(#{chart_ids.join(',')})" }
|
33
|
+
end
|
34
|
+
|
35
|
+
def route
|
36
|
+
"chart/"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Superset
|
4
|
+
module Chart
|
5
|
+
class Delete < Superset::Request
|
6
|
+
attr_reader :chart_id
|
7
|
+
|
8
|
+
def initialize(chart_id: )
|
9
|
+
@chart_id = chart_id
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform
|
13
|
+
raise InvalidParameterError, "chart_id integer is required" unless chart_id.present? && chart_id.is_a?(Integer)
|
14
|
+
|
15
|
+
logger.info("Attempting to delete chart with id: #{chart_id}")
|
16
|
+
response
|
17
|
+
end
|
18
|
+
|
19
|
+
def response
|
20
|
+
@response ||= client.delete(route)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def route
|
26
|
+
"chart/#{chart_id}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Superset
|
2
|
+
module Chart
|
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 result
|
21
|
+
[ super ]
|
22
|
+
end
|
23
|
+
|
24
|
+
def datasource_id
|
25
|
+
if result.first['query_context'].present? && JSON.parse(result.first['query_context'])['datasource'].present?
|
26
|
+
JSON.parse(result.first['query_context'])['datasource']['id']
|
27
|
+
elsif result.first['params'].present? && JSON.parse(result.first['params'])['datasource'].present?
|
28
|
+
JSON.parse(result.first['params'])['datasource'].match(/^\d+/)[0].to_i
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def owner_ids
|
33
|
+
result.first['owners'].map{|o| o['id']}
|
34
|
+
end
|
35
|
+
|
36
|
+
def params
|
37
|
+
JSON.parse(result.first['params']) if result.first['params'].present?
|
38
|
+
end
|
39
|
+
|
40
|
+
def query_context
|
41
|
+
JSON.parse(result.first['query_context']) if result.first['query_context'].present?
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def route
|
47
|
+
"chart/#{id}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def list_attributes
|
51
|
+
%w(id slice_name owners)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Superset
|
2
|
+
module Chart
|
3
|
+
class List < Superset::Request
|
4
|
+
|
5
|
+
attr_reader :name_contains, :dashboard_id_eq, :dataset_id_eq
|
6
|
+
|
7
|
+
def initialize(page_num: 0, name_contains: '', dashboard_id_eq: '', dataset_id_eq: '')
|
8
|
+
@name_contains = name_contains
|
9
|
+
@dashboard_id_eq = dashboard_id_eq
|
10
|
+
@dataset_id_eq = dataset_id_eq
|
11
|
+
super(page_num: page_num)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.call
|
15
|
+
self.new.list
|
16
|
+
end
|
17
|
+
|
18
|
+
def list_with_dashboards
|
19
|
+
puts Terminal::Table.new(
|
20
|
+
title: title,
|
21
|
+
headings: list_dashboard_attributes.map(&:to_s).map(&:humanize),
|
22
|
+
rows: rows_with_dashboards
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def rows_with_dashboards
|
27
|
+
result.map do |d|
|
28
|
+
list_dashboard_attributes.map { |la| d[la].to_s }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def route
|
35
|
+
"chart/?q=(#{query_params})"
|
36
|
+
end
|
37
|
+
|
38
|
+
def filters
|
39
|
+
# TODO filtering across all classes needs a refactor to support multiple options in a more flexible way
|
40
|
+
filter_set = []
|
41
|
+
filter_set << "(col:slice_name,opr:ct,value:'#{name_contains}')" if name_contains.present?
|
42
|
+
filter_set << "(col:dashboards,opr:rel_m_m,value:#{dashboard_id_eq})" if dashboard_id_eq.present? # rel many to many
|
43
|
+
filter_set << "(col:datasource_id,opr:eq,value:#{dataset_id_eq})" if dataset_id_eq.present?
|
44
|
+
|
45
|
+
unless filter_set.empty?
|
46
|
+
"filters:!(" + filter_set.join(',') + "),"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def list_attributes
|
51
|
+
['id', 'slice_name', 'datasource_id', 'datasource_name_text', 'created_by_name']
|
52
|
+
end
|
53
|
+
|
54
|
+
def list_dashboard_attributes
|
55
|
+
['id', 'slice_name', 'dashboards']
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# In the context of a chart, dataset and datasource are the same thing
|
2
|
+
|
3
|
+
module Superset
|
4
|
+
module Chart
|
5
|
+
class UpdateDataset < Superset::Request
|
6
|
+
|
7
|
+
attr_reader :chart_id, :target_dataset_id, :target_dashboard_id
|
8
|
+
|
9
|
+
def initialize(chart_id: , target_dataset_id: , target_dashboard_id: nil)
|
10
|
+
@chart_id = chart_id
|
11
|
+
@target_dataset_id = target_dataset_id
|
12
|
+
@target_dashboard_id = target_dashboard_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def perform
|
16
|
+
validate_proposed_changes
|
17
|
+
|
18
|
+
response
|
19
|
+
|
20
|
+
if result['datasource_id'] == target_dataset_id
|
21
|
+
"Successfully updated chart #{chart_id} to the target dataset #{target_dataset_id}"
|
22
|
+
else
|
23
|
+
"Error: Failed to update chart #{chart_id} to #{target_dataset_id}"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
def response
|
29
|
+
@response ||= client.put(route, params_updated)
|
30
|
+
end
|
31
|
+
|
32
|
+
def params_updated
|
33
|
+
@params_updated ||= begin
|
34
|
+
new_params = {}
|
35
|
+
# chart updates to point to the new target dataset
|
36
|
+
new_params.merge!("datasource_id": target_dataset_id) # point to the new dataset id
|
37
|
+
new_params.merge!("datasource_type": 'table') # type of dataset ?? not sure of other options
|
38
|
+
new_params.merge!("owners": chart.owner_ids ) # expects an array of user ids
|
39
|
+
|
40
|
+
new_params.merge!("params": updated_chart_params.to_json) # updated to point to the new params
|
41
|
+
query_context = updated_chart_query_context
|
42
|
+
if query_context
|
43
|
+
new_params.merge!("query_context": query_context.to_json) # update to point to the new query context
|
44
|
+
new_params.merge!("query_context_generation": true) # new param set to true to regenerate the query context
|
45
|
+
end
|
46
|
+
|
47
|
+
new_params
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def chart
|
54
|
+
@chart ||= Superset::Chart::Get.new(chart_id).perform
|
55
|
+
end
|
56
|
+
|
57
|
+
def validate_proposed_changes
|
58
|
+
raise "Error: chart_id integer is required" unless chart_id.present? && chart_id.is_a?(Integer)
|
59
|
+
raise "Error: target_dataset_id integer is required" unless target_dataset_id.present? && target_dataset_id.is_a?(Integer)
|
60
|
+
# validate schema ???
|
61
|
+
end
|
62
|
+
|
63
|
+
def updated_chart_params
|
64
|
+
chart_params = chart.params # init with current chart params
|
65
|
+
chart_params['datasource'] = chart_params['datasource'].sub(source_dataset_id.to_s, target_dataset_id.to_s) # update to point to the new dataset
|
66
|
+
chart_params['dashboards'] = [target_dashboard_id] if target_dashboard_id.present? # update to point to the new dashboard
|
67
|
+
chart_params
|
68
|
+
end
|
69
|
+
|
70
|
+
def updated_chart_query_context
|
71
|
+
if chart.query_context.present? # not all charts have a query context
|
72
|
+
chart_query_context = chart.query_context # init with source chart query context
|
73
|
+
chart_query_context['datasource']['id'] = target_dataset_id # update to point to the new dataset
|
74
|
+
chart_query_context['form_data']['datasource'] = chart_query_context['form_data']['datasource']
|
75
|
+
.sub(source_dataset_id.to_s, target_dataset_id.to_s) # update to point to the new dataset
|
76
|
+
chart_query_context['form_data']['dashboards'] = [target_dashboard_id] if target_dashboard_id.present? # update to point to the new dashboard
|
77
|
+
chart_query_context
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def source_dataset_id
|
82
|
+
chart.datasource_id
|
83
|
+
end
|
84
|
+
|
85
|
+
def route
|
86
|
+
"chart/#{chart_id}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Superset
|
2
|
+
class Client < Happi::Client
|
3
|
+
include Credential::ApiUser
|
4
|
+
|
5
|
+
attr_reader :authenticator
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@authenticator = Superset::Authenticator.new(credentials)
|
9
|
+
super(log_level: :debug, host: superset_host)
|
10
|
+
end
|
11
|
+
|
12
|
+
def access_token
|
13
|
+
@access_token ||= authenticator.access_token
|
14
|
+
end
|
15
|
+
|
16
|
+
def superset_host
|
17
|
+
@superset_host ||= authenticator.superset_host
|
18
|
+
end
|
19
|
+
|
20
|
+
# TODO: Happi has not got a put method yet
|
21
|
+
def put(resource, params = {})
|
22
|
+
call(:put, url(resource), param_check(params))
|
23
|
+
.body.with_indifferent_access
|
24
|
+
end
|
25
|
+
|
26
|
+
# TODO: Happi is not surfacing the errors correctly overriding raise_error for now
|
27
|
+
def raise_error(response)
|
28
|
+
message =
|
29
|
+
if response.body['errors']
|
30
|
+
response.body['errors']
|
31
|
+
else
|
32
|
+
response.body
|
33
|
+
end
|
34
|
+
|
35
|
+
puts "API Error: #{message}" # display the error message for console debugging
|
36
|
+
# binding.pry # helpfull to debug the response
|
37
|
+
|
38
|
+
raise errors[response.status].new(message, response) # message is not being surfaced from Happi correctly, :(
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def connection
|
44
|
+
@connection ||= Faraday.new(superset_host) do |f|
|
45
|
+
f.authorization :Bearer, access_token
|
46
|
+
f.use FaradayMiddleware::ParseJson, content_type: 'application/json'
|
47
|
+
f.request :json
|
48
|
+
|
49
|
+
f.adapter :net_http
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Superset
|
2
|
+
module Credential
|
3
|
+
module ApiUser
|
4
|
+
|
5
|
+
def credentials
|
6
|
+
{
|
7
|
+
"username": api_username,
|
8
|
+
"password": api_password,
|
9
|
+
"provider": "db",
|
10
|
+
"refresh": false
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def api_username
|
17
|
+
ENV['SUPERSET_API_USERNAME']
|
18
|
+
end
|
19
|
+
|
20
|
+
def api_password
|
21
|
+
ENV['SUPERSET_API_PASSWORD']
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Superset
|
2
|
+
module Credential
|
3
|
+
module EmbeddedUser
|
4
|
+
|
5
|
+
def credentials
|
6
|
+
{
|
7
|
+
"username": embedded_username,
|
8
|
+
"password": embedded_password,
|
9
|
+
"provider": "db",
|
10
|
+
"refresh": false
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def embedded_username
|
17
|
+
ENV['SUPERSET_EMBEDDED_USERNAME']
|
18
|
+
end
|
19
|
+
|
20
|
+
def embedded_password
|
21
|
+
ENV['SUPERSET_EMBEDDED_PASSWORD']
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# TODO: the gui delete has a confirmation step, this API call does not.
|
4
|
+
# Potentially we could add a confirm_delete parameter to the constructor that would ensure that all dashboards either
|
5
|
+
# 1 have an expected set of charts or filters before deleting
|
6
|
+
# 2 or do not have any charts or filters linked to them
|
7
|
+
# ( not sure if this needed at this point )
|
8
|
+
|
9
|
+
# NOTE: deletes the Dashboard Only. Use Dashboard::BulkDeleteCascade to delete all related objects
|
10
|
+
module Superset
|
11
|
+
module Dashboard
|
12
|
+
class BulkDelete < Superset::Request
|
13
|
+
attr_reader :dashboard_ids
|
14
|
+
|
15
|
+
def initialize(dashboard_ids: [])
|
16
|
+
@dashboard_ids = dashboard_ids
|
17
|
+
end
|
18
|
+
|
19
|
+
def perform
|
20
|
+
raise InvalidParameterError, "dashboard_ids array of integers expected" unless dashboard_ids.is_a?(Array)
|
21
|
+
raise InvalidParameterError, "dashboard_ids array must contain Integer only values" unless dashboard_ids.all? { |item| item.is_a?(Integer) }
|
22
|
+
|
23
|
+
logger.info("Attempting to delete dashboards with id: #{dashboard_ids.join(', ')}")
|
24
|
+
response
|
25
|
+
end
|
26
|
+
|
27
|
+
def response
|
28
|
+
@response ||= client.delete(route, params)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def params
|
34
|
+
{ q: "!(#{dashboard_ids.join(',')})" }
|
35
|
+
end
|
36
|
+
|
37
|
+
def route
|
38
|
+
"dashboard/"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|