-
Notifications
You must be signed in to change notification settings - Fork 2
API Compare Dashboard Report #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 8 commits
15009be
1f22dc0
3c3c33d
251552d
e74e49a
9209a21
b50cc16
2c89619
8566f2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# Superset has a 100 result limit for requests | ||
# This is a wrapper for Superset::Dashboard::List to recursively list all dashboards | ||
|
||
# TODO - would be very handy to create a parent class for this | ||
# to then be able to use the same pattern for other ::List classes | ||
|
||
module Superset | ||
module Dashboard | ||
class ListAll | ||
include Display | ||
|
||
def initialize(**kwargs) | ||
kwargs.each do |key, value| | ||
instance_variable_set("@#{key}", value) | ||
self.class.attr_reader key | ||
end | ||
end | ||
|
||
def constructor_args | ||
instance_variables.each_with_object({}) do |var, hash| | ||
hash[var.to_s.delete('@').to_sym] = instance_variable_get(var) | ||
end | ||
end | ||
|
||
def perform | ||
page_num = 0 | ||
boards = [] | ||
boards << next_group = Dashboard::List.new(page_num: page_num, **constructor_args).result | ||
while !next_group.empty? | ||
boards << next_group = Dashboard::List.new(page_num: page_num += 1, **constructor_args).result | ||
end | ||
@result = boards.flatten | ||
end | ||
|
||
def result | ||
@result ||= [] | ||
end | ||
|
||
def rows | ||
result.map do |d| | ||
list_attributes.map do |la| | ||
la == :url ? "#{superset_host}#{d[la]}" : d[la] | ||
end | ||
end | ||
end | ||
|
||
def ids | ||
result.map { |d| d[:id] } | ||
end | ||
|
||
private | ||
|
||
def list_attributes | ||
[:id, :dashboard_title, :status, :url] | ||
end | ||
|
||
def superset_host | ||
ENV['SUPERSET_HOST'] | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,7 +38,7 @@ def database_id | |
end | ||
|
||
def sql | ||
['sql'] | ||
result['sql'] | ||
end | ||
|
||
private | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
# Creates a log report on a set of dashboards | ||
# providing count of charts, datasets, and databases used in each dashboard | ||
# as well as optional data sovereignty information | ||
|
||
# Data Sovereignty in this context requires that all datasets used in a dashboard are from one database schema only. | ||
# Primarily used to identify potential issues with embedded dashboards where data sovereignty is a concern. | ||
|
||
# Usage: | ||
# Superset::Services::DashboardReport.new(dashboard_ids: [1,2,3]).perform | ||
|
||
module Superset | ||
module Services | ||
class DashboardReport | ||
|
||
attr_reader :dashboard_ids, :report_on_data_sovereignty_only | ||
|
||
def initialize(dashboard_ids: [], report_on_data_sovereignty_only: true) | ||
@dashboard_ids = dashboard_ids | ||
@report_on_data_sovereignty_only = report_on_data_sovereignty_only | ||
end | ||
|
||
def perform | ||
create_dashboard_report | ||
load_data_sovereignty_issues | ||
|
||
report_on_data_sovereignty_only ? display_data_sovereignty_report : @report | ||
end | ||
|
||
private | ||
|
||
def display_data_sovereignty_report | ||
# filter by dashboards where | ||
# 1. A filter dataset is not part of the dashboard datasets (might be ok for some cases, ie a dummy dataset listing dates only) | ||
# 2. There is more than one distinct dataset schema (never ok for embedded dashboards where the expected schema num is only one) | ||
|
||
puts "Data Sovereignty Report" | ||
puts "-----------------------" | ||
puts "Possible Invalid Dashboard Datasets: #{@data_sovereignty_issues.count}" | ||
@data_sovereignty_issues | ||
end | ||
|
||
# possible data sovereignty issues | ||
def load_data_sovereignty_issues | ||
@data_sovereignty_issues ||= begin | ||
@report.map do |dashboard| | ||
reasons = [] | ||
chart_dataset_ids = dashboard[:datasets][:chart_datasets].map{|d| d[:id]} | ||
|
||
# add WARNING msg if any filters datasets are not part of the chart datasets | ||
unknown_datasets = dashboard[:filters][:filter_dataset_ids] - chart_dataset_ids | ||
if unknown_datasets.any? | ||
reasons << "WARNING: One or more filter datasets is not included in chart datasets for " \ | ||
"filter dataset ids: #{unknown_datasets.join(', ')}." | ||
reasons << "DETAILS: #{unknown_dataset_details(unknown_datasets)}" | ||
end | ||
|
||
# add ERROR msg if multiple chart dataset schemas are found, ie all datasets should be sourced from the same db schema | ||
chart_dataset_schemas = dashboard[:datasets][:chart_datasets].map{|d| d[:schema]}.uniq | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also include the filter datasets here to see if filter dataset schema is different from chart dataset schema ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @soundarya-mv nice pickup, and yes we should. |
||
if chart_dataset_schemas.count > 1 | ||
reasons << "ERROR: Multiple distinct chart dataset schemas found. Expected 1. Found #{chart_dataset_schemas.count}. " \ | ||
"schema names: #{chart_dataset_schemas.join(', ') }" | ||
end | ||
|
||
{ reasons: reasons, dashboard: dashboard } if reasons.any? | ||
end.compact | ||
end | ||
end | ||
|
||
def unknown_dataset_details(unknown_datasets) | ||
unknown_datasets.map do |dataset_id| | ||
d = Superset::Dataset::Get.new(dataset_id) | ||
d.result | ||
{ id: d.id, name: d.title } | ||
rescue Happi::Error::NotFound => e | ||
{ id: dataset_id, name: '>>>> ERROR: DATASET DOES NOT EXIST <<<<' } | ||
end | ||
end | ||
|
||
def create_dashboard_report | ||
@report ||= begin | ||
dashboard_ids.map do |dashboard_id| | ||
dashboard = dashboard_result(dashboard_id) | ||
{ | ||
dashboard_id: dashboard_id, | ||
dashboard_title: dashboard['dashboard_title'], | ||
dashboard_url: dashboard['url'], | ||
dashboard_tags: dashboard_tags(dashboard), | ||
filters: filter_details(dashboard), | ||
charts: chart_count(dashboard), | ||
datasets: dataset_details(dashboard_id), | ||
} | ||
end | ||
end | ||
end | ||
|
||
def filter_details(dashboard) | ||
{ | ||
filter_count: filter_count(dashboard), | ||
filter_dataset_ids: filter_datasets(dashboard) | ||
} | ||
end | ||
|
||
def filter_count(dashboard) | ||
dashboard['json_metadata']['native_filter_configuration']&.count || 0 | ||
end | ||
|
||
def filter_datasets(dashboard) | ||
dashboard['json_metadata']['native_filter_configuration'].map do |filter| | ||
filter['targets'].map{|d| d['datasetId']} if filter['type'] == 'NATIVE_FILTER' | ||
end.flatten.compact.uniq | ||
end | ||
|
||
def chart_count(dashboard) | ||
dashboard['json_metadata']['chart_configuration'].count | ||
end | ||
|
||
def dataset_details(dashboard_id) | ||
datasets = Superset::Dashboard::Datasets::List.new(dashboard_id: dashboard_id).rows_hash | ||
{ | ||
dataset_count: datasets.count, | ||
chart_datasets: datasets | ||
} | ||
end | ||
|
||
def dashboard_tags(dashboard) | ||
dashboard['tags'].map{|t| t['name']}.join('|') | ||
end | ||
|
||
def dashboard_result(dashboard_id) | ||
# convert json_metadata within result to a hash | ||
board = Superset::Dashboard::Get.new(dashboard_id) | ||
board.result['json_metadata'] = JSON.parse(board.result['json_metadata']) | ||
board.result['url'] = board.url # add full url to the dashboard result | ||
board.result | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If chart_datasets also contains filter_datasets, then won't the below condition be false always ?