Skip to content

Commit 7b55c75

Browse files
authored
Merge pull request #17 from rdytech/NEP-17492-validate-and-duplicate-filters
[NEP-17492] Validate and duplicate filters to new dashboard
2 parents af9ad4f + 85a1fad commit 7b55c75

File tree

6 files changed

+203
-15
lines changed

6 files changed

+203
-15
lines changed

Gemfile.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ GIT
1414
PATH
1515
remote: .
1616
specs:
17-
superset (0.1.1)
17+
superset (0.1.2)
1818
faraday (~> 1.0)
1919
faraday-multipart (~> 1.0)
2020
json (~> 2.6)
@@ -144,6 +144,7 @@ GEM
144144
PLATFORMS
145145
aarch64-linux
146146
arm64-darwin-22
147+
arm64-darwin-23
147148

148149
DEPENDENCIES
149150
happi!

lib/superset/chart/update_dataset.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@ def params_updated
3737
new_params.merge!("owners": chart.owner_ids ) # expects an array of user ids
3838

3939
new_params.merge!("params": updated_chart_params.to_json) # updated to point to the new params
40-
new_params.merge!("query_context": updated_chart_query_context.to_json) # update to point to the new query context
41-
new_params.merge!("query_context_generation": true) # new param set to true to regenerate the query context
40+
query_context = updated_chart_query_context
41+
if query_context
42+
new_params.merge!("query_context": query_context.to_json) # update to point to the new query context
43+
new_params.merge!("query_context_generation": true) # new param set to true to regenerate the query context
44+
end
4245

4346
new_params
4447
end

lib/superset/dashboard/put.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
# frozen_string_literal: true
3+
4+
module Superset
5+
module Dashboard
6+
class Put < Superset::Request
7+
8+
attr_reader :target_dashboard_id, :params
9+
10+
def initialize(target_dashboard_id:, params:)
11+
@target_dashboard_id = target_dashboard_id
12+
@params = params
13+
end
14+
15+
def perform
16+
raise "Error: target_dashboard_id integer is required" unless target_dashboard_id.present? && target_dashboard_id.is_a?(Integer)
17+
raise "Error: params hash is required" unless params.present? && params.is_a?(Hash)
18+
19+
response
20+
Superset::Dashboard::Get.new(id).perform
21+
end
22+
23+
def response
24+
@response ||= client.put(route, params)
25+
end
26+
27+
def id
28+
response["result"]["id"]
29+
end
30+
31+
private
32+
33+
def route
34+
"dashboard/#{target_dashboard_id}"
35+
end
36+
end
37+
end
38+
end

lib/superset/services/duplicate_dashboard.rb

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ def perform
3434
# Update the Charts on the New Dashboard with the New Datasets
3535
update_charts_with_new_datasets
3636

37+
# Duplicate filters to the new target schema and target database
38+
duplicate_source_dashboard_filters
39+
3740
created_embedded_config
3841

3942
end_log_message
@@ -63,7 +66,13 @@ def dataset_duplication_tracker
6366

6467
def update_charts_with_new_datasets
6568
# get all chart ids for the new dashboard
66-
chart_ids_list = Superset::Dashboard::Charts::List.new(new_dashboard.id).chart_ids
69+
new_charts_list = Superset::Dashboard::Charts::List.new(new_dashboard.id).result
70+
chart_ids_list = new_charts_list&.map { |r| r['id'] }&.compact
71+
original_charts = Superset::Dashboard::Charts::List.new(source_dashboard_id).result.map { |r| [r['slice_name'], r['id']] }.to_h
72+
new_charts = new_charts_list.map { |r| [r['id'], r['slice_name']] }.to_h
73+
json_metadata = new_dashboard.result['json_metadata']
74+
75+
return unless chart_ids_list.any?
6776

6877
# for each chart, update the charts current dataset_id with the new dataset_id
6978
chart_ids_list.each do |chart_id|
@@ -75,8 +84,14 @@ def update_charts_with_new_datasets
7584
new_dataset_id = dataset_duplication_tracker.find { |dataset| dataset[:source_dataset_id] == current_chart_dataset_id }&.fetch(:new_dataset_id, nil)
7685

7786
# update the chart to target the new dataset_id
78-
Superset::Chart::UpdateDataset.new(chart_id: chart_id, target_dataset_id: new_dataset_id).response
87+
Superset::Chart::UpdateDataset.new(chart_id: chart_id, target_dataset_id: new_dataset_id).perform
88+
89+
# update json metadata
90+
original_chart_id = original_charts[new_charts[chart_id]]
91+
json_metadata.gsub!(original_chart_id.to_s, chart_id.to_s)
7992
end
93+
94+
Superset::Dashboard::Put.new(target_dashboard_id: new_dashboard.id, params: { "json_metadata" => json_metadata }).perform
8095
end
8196

8297
def duplicate_source_dashboard_datasets
@@ -88,8 +103,23 @@ def duplicate_source_dashboard_datasets
88103
dataset_duplication_tracker << { source_dataset_id: dataset[:id], new_dataset_id: new_dataset_id }
89104

90105
# update the new dataset with the target schema and target database
91-
Superset::Dataset::UpdateSchema.new(source_dataset_id: new_dataset_id, target_database_id: target_database_id, target_schema: target_schema).response
106+
Superset::Dataset::UpdateSchema.new(source_dataset_id: new_dataset_id, target_database_id: target_database_id, target_schema: target_schema).perform
107+
end
108+
end
109+
110+
def duplicate_source_dashboard_filters
111+
return unless source_dashboard_filters.length.positive?
112+
113+
json_metadata = Superset::Dashboard::Get.new(new_dashboard.id).json_metadata
114+
configuration = json_metadata['native_filter_configuration']&.map do |filter_config|
115+
targets = filter_config['targets']
116+
target_filter_dataset_id = dataset_duplication_tracker.find { |d| d[:source_dataset_id] == targets.first["datasetId"] }&.fetch(:new_dataset_id, nil)
117+
filter_config['targets'] = [targets.first.merge({ "datasetId"=> target_filter_dataset_id })]
118+
filter_config
92119
end
120+
121+
json_metadata['native_filter_configuration'] = configuration || []
122+
Superset::Dashboard::Put.new(target_dashboard_id: new_dashboard.id, params: { "json_metadata" => json_metadata.to_json }).perform
93123
end
94124

95125
def new_dashboard
@@ -125,10 +155,14 @@ def validate_params
125155
# schema validations
126156
raise ValidationError, "Schema #{target_schema} does not exist in target database: #{target_database_id}" unless target_database_available_schemas.include?(target_schema)
127157
raise ValidationError, "The source_dashboard_id #{source_dashboard_id} datasets are required to point to one schema only. Actual schema list is #{source_dashboard_schemas.join(',')}" if source_dashboard_has_more_than_one_schema?
158+
raise ValidationError, "The source_dashboard_id #{source_dashboard_id} filters point to more than one schema." if any_unpermitted_filters?
128159

129160
# new dataset validations
130161
raise ValidationError, "DATASET NAME CONFLICT: The Target Schema #{target_schema} already has existing datasets named: #{target_schema_matching_dataset_names.join(',')}" unless target_schema_matching_dataset_names.empty?
162+
end
131163

164+
def source_dashboard
165+
@source_dashboard ||= Superset::Dashboard::Get.new(source_dashboard_id)
132166
end
133167

134168
def target_database_available_schemas
@@ -162,6 +196,27 @@ def target_schema_matching_dataset_names
162196
end.flatten.compact
163197
end
164198

199+
def allowed_filters(dashboard_id)
200+
Superset::Dashboard::Datasets::List.new(dashboard_id).result
201+
end
202+
203+
def source_allowed_filters
204+
@source_allowed_filters ||= Superset::Dashboard::Datasets::List.new(source_dashboard_id).result
205+
end
206+
207+
def source_dashboard_filters
208+
filters_configuration = JSON.parse(source_dashboard.result['json_metadata'])['native_filter_configuration'] || []
209+
return Array.new unless filters_configuration && filters_configuration.any?
210+
211+
filters = filters_configuration.map { |c| c['targets'] }.flatten.compact
212+
end
213+
214+
def any_unpermitted_filters?
215+
source_allowed_filter_ids = source_allowed_filters.map { |r| r["id"] }.compact.uniq
216+
source_dashboard_filter_ids = source_dashboard_filters.map { |t| t["datasetId"] }.compact.uniq
217+
(source_dashboard_filter_ids - source_allowed_filter_ids).any?
218+
end
219+
165220
def logger
166221
@logger ||= Superset::Logger.new
167222
end
@@ -179,4 +234,4 @@ def end_log_message
179234
end
180235
end
181236
end
182-
end
237+
end
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
require 'spec_helper'
2+
3+
RSpec.describe Superset::Dashboard::Put do
4+
subject { described_class.new(target_dashboard_id: dashboard_id, params: params) }
5+
let(:dashboard_id) { 1 }
6+
let(:params) {{
7+
"json_metadata" => {
8+
"key1" => "value1",
9+
"positions" => { "key2" => "value2" }
10+
}.to_json
11+
}}
12+
13+
let(:new_dashboard_id) { 2 }
14+
let(:new_dashboard_instance) { instance_double("Superset::Dashboard::Get") }
15+
16+
before do
17+
allow(subject).to receive(:response).and_return( { "result" =>
18+
{"id"=>new_dashboard_id, "last_modified_time"=>1708484547.0} }
19+
)
20+
end
21+
22+
describe 'perform' do
23+
context 'with valid params' do
24+
before do
25+
allow(Superset::Dashboard::Get).to receive(:new).with(new_dashboard_id).and_return(new_dashboard_instance)
26+
allow(new_dashboard_instance).to receive(:perform).and_return(new_dashboard_instance)
27+
end
28+
29+
it 'returns the new dashboard object' do
30+
expect(subject.perform).to be new_dashboard_instance
31+
end
32+
end
33+
34+
context 'with invalid params' do
35+
context 'when source_dashboard_id is not an integer' do
36+
let(:dashboard_id) { 'q' }
37+
38+
it 'raises an error' do
39+
expect { subject.perform }.to raise_error("Error: target_dashboard_id integer is required")
40+
end
41+
end
42+
43+
context 'when source_dashboard_id is not present' do
44+
let(:dashboard_id) { nil }
45+
46+
it 'raises an error' do
47+
expect { subject.perform }.to raise_error("Error: target_dashboard_id integer is required")
48+
end
49+
end
50+
51+
context 'when params is not a hash' do
52+
let(:params) { 'q' }
53+
54+
it 'raises an error' do
55+
expect { subject.perform }.to raise_error("Error: params hash is required")
56+
end
57+
end
58+
59+
context 'when params is nil' do
60+
let(:params) { nil }
61+
62+
it 'raises an error' do
63+
expect { subject.perform }.to raise_error("Error: params hash is required")
64+
end
65+
end
66+
end
67+
end
68+
end

spec/superset/services/duplicate_dashboard_spec.rb

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,20 @@
1111
let(:target_schema) { 'schema_one' }
1212
let(:target_database_id) { 6 }
1313
let(:target_database_available_schemas) { ['schema_one', 'schema_two', 'schema_three'] }
14-
1514
let(:source_dataset_1) { 101 }
1615
let(:source_dataset_2) { 102 }
1716
let(:source_dashboard_datasets) {[
1817
{id: source_dataset_1, datasource_name: "Dataset 1", schema: "schema1", database: {id: 9, name: "db_9", backend: "postgresql"}},
1918
{id: source_dataset_2, datasource_name: "Dataset 2", schema: "schema1", database: {id: 9, name: "db_9", backend: "postgresql"}}
2019
]}
20+
let(:json_metadata) { { 'native_filter_configuration' => [{ 'targets' => [{ 'datasetId' => 201 }]}]} }
21+
let(:new_json_metadata) { { 'native_filter_configuration' => [{ 'targets' => [{ 'datasetId' => 201 }]}]} }
22+
let(:source_allowed_filters) { [{'id' => 201}] }
23+
let(:source_dashboard_filters) {[{ 'datasetId' => 201 }]}
24+
let(:dataset_duplication_tracker) { [{ source_dataset_id: 201, new_dataset_id: 201 }] }
2125

2226
let(:new_dashboard_id) { 2 }
23-
let(:new_dashboard) { double('new_dashboard', id: new_dashboard_id, url: "http://superset-host.com/superset/dashboard/#{new_dashboard_id}") }
27+
let(:new_dashboard) { double('new_dashboard', id: new_dashboard_id, url: "http://superset-host.com/superset/dashboard/#{new_dashboard_id}", json_metadata: json_metadata) }
2428

2529
let(:new_dataset_1) { 201 }
2630
let(:new_dataset_2) { 202 }
@@ -35,30 +39,40 @@
3539
allow(subject).to receive(:new_dashboard).and_return(new_dashboard)
3640
allow(subject).to receive(:source_dashboard_datasets).and_return(source_dashboard_datasets)
3741
allow(subject).to receive(:target_schema_matching_dataset_names).and_return(existing_target_datasets_list)
42+
allow(subject).to receive(:source_allowed_filters).and_return(source_allowed_filters)
43+
allow(subject).to receive(:source_dashboard_filters).and_return(source_dashboard_filters)
44+
allow(subject).to receive(:dataset_duplication_tracker).and_return(dataset_duplication_tracker)
45+
allow(new_dashboard).to receive(:result).and_return({ 'json_metadata' => json_metadata.to_json })
3846
end
3947

4048
describe '#perform' do
4149
context 'with valid params' do
42-
4350
before do
4451
# duplicating the current datasets
4552
expect(Superset::Dataset::Duplicate).to receive(:new).with(source_dataset_id: source_dataset_1, new_dataset_name: "Dataset 1 (COPY)").and_return(double(perform: new_dataset_1))
4653
expect(Superset::Dataset::Duplicate).to receive(:new).with(source_dataset_id: source_dataset_2, new_dataset_name: "Dataset 2 (COPY)").and_return(double(perform: new_dataset_2))
4754

4855
# updating the new datasets to point to the target schema and target database
49-
expect(Superset::Dataset::UpdateSchema).to receive(:new).with(source_dataset_id: new_dataset_1, target_database_id: target_database_id, target_schema: target_schema).and_return(double(response: true))
50-
expect(Superset::Dataset::UpdateSchema).to receive(:new).with(source_dataset_id: new_dataset_2, target_database_id: target_database_id, target_schema: target_schema).and_return(double(response: true))
56+
expect(Superset::Dataset::UpdateSchema).to receive(:new).with(source_dataset_id: new_dataset_1, target_database_id: target_database_id, target_schema: target_schema).and_return(double(perform: new_dataset_1))
57+
expect(Superset::Dataset::UpdateSchema).to receive(:new).with(source_dataset_id: new_dataset_2, target_database_id: target_database_id, target_schema: target_schema).and_return(double(perform: new_dataset_2))
5158

5259
# getting the list of charts for the source dashboard
53-
allow(Superset::Dashboard::Charts::List).to receive(:new).with(new_dashboard_id).and_return(double(chart_ids: [new_chart_1, new_chart_2]))
60+
allow(Superset::Dashboard::Charts::List).to receive(:new).with(source_dashboard_id).and_return(double(result: [{ 'slice_name' => "test", "id" => 3001}, { 'slice_name' => "test", "id" => 3002}], chart_ids: [new_chart_1, new_chart_2]))
61+
allow(Superset::Dashboard::Charts::List).to receive(:new).with(new_dashboard_id).and_return(double(result: [{ 'slice_name' => "test", "id" => 3001}, { 'slice_name' => "test", "id" => 3002}]))
5462

5563
# getting the current dataset_id for the new charts .. still pointing to the old datasets
5664
expect(Superset::Chart::Get).to receive(:new).with(3001).and_return(double(datasource_id: source_dataset_1))
5765
expect(Superset::Chart::Get).to receive(:new).with(3002).and_return(double(datasource_id: source_dataset_2))
5866

5967
# updating the new charts to point to the new datasets
60-
expect(Superset::Chart::UpdateDataset).to receive(:new).with(chart_id: new_chart_1, target_dataset_id: new_dataset_1).and_return(double(response: true))
61-
expect(Superset::Chart::UpdateDataset).to receive(:new).with(chart_id: new_chart_2, target_dataset_id: new_dataset_2).and_return(double(response: true))
68+
expect(Superset::Chart::UpdateDataset).to receive(:new).with(chart_id: new_chart_1, target_dataset_id: new_dataset_1).and_return(double(perform: true))
69+
expect(Superset::Chart::UpdateDataset).to receive(:new).with(chart_id: new_chart_2, target_dataset_id: new_dataset_2).and_return(double(perform: true))
70+
71+
# get json metadata
72+
expect(Superset::Dashboard::Get).to receive(:new).with(new_dashboard_id).and_return(double(json_metadata: json_metadata))
73+
74+
# update dashboard json metadata
75+
expect(Superset::Dashboard::Put).to receive(:new).twice.with(target_dashboard_id: new_dashboard_id, params: { 'json_metadata' => new_json_metadata.to_json }).and_return(double(perform: true))
6276
end
6377

6478
specify do
@@ -99,6 +113,15 @@
99113
end
100114
end
101115

116+
context 'filters set is outside source schema' do
117+
let(:target_schema) { 'schema_one' }
118+
let(:source_dashboard_filters) { [{ 'datasetId' => 201 }, { 'datasetId' => 203 }] }
119+
120+
specify do
121+
expect { subject.perform }.to raise_error(Superset::Request::ValidationError, "The source_dashboard_id #{source_dashboard_id} filters point to more than one schema.")
122+
end
123+
end
124+
102125
context 'source dashboard datasets use multiple schemas' do
103126
before do
104127
allow(subject).to receive(:source_dashboard_schemas).and_return(['schema_one', 'schema_five'])

0 commit comments

Comments
 (0)