From adea7401a8177f0d64bfeadbf9aa9c01f52e573b Mon Sep 17 00:00:00 2001 From: Libor Pichler Date: Thu, 19 Nov 2020 15:49:05 +0100 Subject: [PATCH] Add widget set endpoint --- app/controllers/api/widget_sets_controller.rb | 134 ++++++++++++++++ config/api.yml | 45 ++++++ spec/requests/widget_sets_spec.rb | 146 ++++++++++++++++++ 3 files changed, 325 insertions(+) create mode 100644 app/controllers/api/widget_sets_controller.rb create mode 100644 spec/requests/widget_sets_spec.rb diff --git a/app/controllers/api/widget_sets_controller.rb b/app/controllers/api/widget_sets_controller.rb new file mode 100644 index 0000000000..3066fece11 --- /dev/null +++ b/app/controllers/api/widget_sets_controller.rb @@ -0,0 +1,134 @@ +module Api + class WidgetSetsController < BaseController + REQUIRED_FIELDS_TO_COPY = %w[name].freeze + ALLOWED_FIELDS = REQUIRED_FIELDS_TO_COPY + %w[group description guid read_only set_data].freeze + SET_DATA_COLS = %i[col1 col2 col3].freeze + + def init_set_data(set_data) + set_data ||= {} + new_set_data ||= {} + new_set_data[:col1] = set_data['col1'] || [] + new_set_data[:col2] = set_data['col2'] || [] + new_set_data[:col3] = set_data['col3'] || [] + new_set_data[:reset_upon_login] = !!set_data['reset_upon_login'] + new_set_data[:locked] = !!set_data['locked'] + new_set_data + end + + def init_widget_set(data) + data['set_type'] = "MiqWidgetSet" + data['owner_type'] = "MiqGroup" + data['set_data'] = init_set_data(data['set_data']) + data + end + + def validate_description(group_id, description) + widget_set_exists_in_group = MiqWidgetSet.exists?(:owner_id => group_id, :description => description) + raise ArgumentError, "Description(Tab Title) must be unique for this group" if widget_set_exists_in_group + end + + def validate_widgets_in_set_data(set_data) + widget_ids = SET_DATA_COLS.map { |x| set_data[x] }.flatten.compact + raise ArgumentError, "One widget must be selected(set_data)" if widget_ids.empty? + + filtered_widget_ids = MiqWidget.where(:id => widget_ids).pluck(:id) + unless filtered_widget_ids.count == widget_ids.count + raise ArgumentError, "Unable to find widget set ids: #{widget_ids - filtered_widget_ids} " + end + end + + def create_resource(type, id = nil, data = nil) + raise_if_unsupported_fields_passed(data) + + raise ArgumentError, "Name cannot contain \"|\"" if data['name'].index('|') + + data = init_widget_set(data) + + validate_description(data['owner_id'], data['description']) + validate_widgets_in_set_data(data['set_data']) + + data['owner_id'] = parse_resource_from(data.delete('group')) + group = resource_search(data['owner_id'], :groups, MiqGroup) + result = nil + group.transaction do + result = super(type, id, data) + group.settings ||= {:dashboard_order => []} + group.settings[:dashboard_order].push(result['id']) + group.save + widget_set_update_members(result) + end + + result + end + + def edit_resource(type, id = nil, data = nil) + raise_if_unsupported_fields_passed(data, ALLOWED_FIELDS - %w[name group]) + + if data['description'] + resource = resource_search(id, type, collection_class(type)) + validate_description(resource.owner_id, data['description']) + end + + data['set_data'] = init_set_data(data['set_data']) + validate_widgets_in_set_data(data['set_data']) + result = nil + collection_class(type).transaction do + result = super(type, id, data) + widget_set_update_members(result) + end + + result + end + + def delete_resource(type, id = nil, data = nil) + klass = collection_class(type) + widget_set = resource_search(id, type, klass) + raise ArgumentError, "Unable to delete read_only widget_set" if widget_set.read_only? + + api_action(type, id) do |klass| + widget_set.transaction do + group = MiqGroup.find(widget_set.owner_id) + group.save if group&.settings.try(:dashboard_order)&.delete(widget_set.id) + + super(type, id, data) + end + + action_result(true, "Dashboard #{widget_set.name} has been successfully deleted.") + end + end + + def copy_resource(type, id = nil, data = nil) + raise ArgumentError, "Required field(s) #{REQUIRED_FIELDS_TO_COPY.join(", ")}" if (REQUIRED_FIELDS_TO_COPY - data.keys).present? + + raise_if_unsupported_fields_passed(data) + + api_action(type, id) do |klass| + widget_set = resource_search(id, type, klass) + group_id = data['id']&.to_i || parse_resource_from(data['group']) || widget_set.group_id + copied_widget_set = MiqWidgetSet.copy_dashboard(widget_set, data['name'], data['description'], group_id) + widget_set_update_members(copied_widget_set) + + action_result(true, "Dashboard #{data['name']} successfully created.") + end + end + + def parse_resource_from(attributes) + return unless attributes + + attributes['id']&.to_i || (attributes['href'] && Api::Href.new(attributes['href']).subject_id) if attributes + end + + def raise_if_unsupported_fields_passed(data, allowed_fields = ALLOWED_FIELDS) + unsupported_fields = data.keys - allowed_fields + raise ArgumentError, "Field(s) #{unsupported_fields.join(", ")} are not supported" if unsupported_fields.present? + end + + def widget_set_update_members(widget_set) + widget_ids = SET_DATA_COLS.collect { |key| widget_set.set_data[key] }.flatten + widgets = Array(MiqWidget.where(:id => widget_ids)) + + widget_set.replace_children(widgets) + widget_set.members.each { |w| w.create_initial_content_for_user(current_user.userid) } # Generate content if not there + end + end +end diff --git a/config/api.yml b/config/api.yml index e5e7af2e60..06053a247d 100644 --- a/config/api.yml +++ b/config/api.yml @@ -4619,6 +4619,51 @@ :identifier: - vm_snapshot_delete - sui_vm_snapshot_delete + :widget_sets: + :description: Dashboards + :identifier: miq_report_dashboard_editor + :options: + - :collection + :verbs: *gpppd + :klass: MiqWidgetSet + :collection_actions: + :get: + - :name: read + :identifier: miq_report_dashboard_editor + :post: + - :name: query + :identifier: miq_report_dashboard_editor + - :name: create + :identifier: db_new + - :name: edit + :identifier: db_edit + - :name: delete + :identifier: db_delete + - :name: copy + :identifier: db_copy + :delete: + - :name: delete + :identifier: db_delete + :resource_actions: + :get: + - :name: read + :identifier: miq_report_dashboard_editor + :post: + - :name: edit + :identifier: db_edit + - :name: delete + :identifier: db_delete + - :name: copy + :identifier: db_copy + :patch: + - :name: edit + :identifier: db_edit + :put: + - :name: edit + :identifier: db_edit + :delete: + - :name: delete + :identifier: db_delete :widgets: :description: Miq Widgets :identifier: miq_report_widget_admin diff --git a/spec/requests/widget_sets_spec.rb b/spec/requests/widget_sets_spec.rb new file mode 100644 index 0000000000..33a2413ad6 --- /dev/null +++ b/spec/requests/widget_sets_spec.rb @@ -0,0 +1,146 @@ +describe "Widget Sets API" do + let(:group) { User.current_user.current_group } + let(:miq_widget_set) { FactoryBot.create(:miq_widget_set, :owner => group) } + let(:miq_widget_set_other) { FactoryBot.create(:miq_widget_set, :owner => group) } + let(:widgets) { FactoryBot.create_list(:miq_widget, 3) } + let(:widget_params) do + { + "name" => "XXX", + "description" => "YYY", + "set_data" => {"col1" => widgets.map(&:id), + "reset_upon_login" => false, + "locked" => false} + } + end + + before do + User.current_user = User.first + end + + context "GET" do + it "returns single" do + api_basic_authorize collection_action_identifier(:widget_sets, :read, :get) + + get(api_widget_set_url(nil, miq_widget_set)) + + expect(response).to have_http_status(:ok) + expect(response.parsed_body['id'].to_i).to eq(miq_widget_set.id) + end + + it "returns all widget sets" do + api_basic_authorize collection_action_identifier(:widget_sets, :read, :get) + + get(api_widget_sets_url) + + expect(response).to have_http_status(:ok) + widget_sets_hrefs = response.parsed_body['resources'].map { |x| x['href'] } + all_widget_sets_hrefs = MiqWidgetSet.all.map { |ws| api_widget_set_url(nil, ws) } + expect(widget_sets_hrefs).to match_array(all_widget_sets_hrefs) + end + + it "doesn't find widget set" do + api_basic_authorize collection_action_identifier(:widget_sets, :read, :get) + + get(api_widget_set_url(nil, 999_999)) + + expect(response).to have_http_status(:not_found) + end + + it "forbids action get for non-super-admin user" do + expect_forbidden_request do + get(api_widget_set_url(nil, miq_widget_set)) + end + end + end + + context "POST" do + let(:group_href) { api_group_url(nil, group) } + + it "creates widget set" do + api_basic_authorize collection_action_identifier(:widget_sets, :create, :post) + + post api_widget_sets_url, :params => gen_request(:create, widget_params.merge('group' => {'href' => group_href})) + + expect(response).to have_http_status(:ok) + + widget_params["set_data"]["col2"] = [] + widget_params["set_data"]["col3"] = [] + expect(response.parsed_body['results'][0].values_at(*widget_params.keys)).to match_array(widget_params.values) + ws = MiqWidgetSet.find(response.parsed_body['results'][0]['id']) + expect(ws.members.map(&:id)).to eq(widgets.map(&:id)) + group.reload + expect(group.settings["dashboard_order"]).to eq([ws.id]) + end + + it "updates widget set" do + api_basic_authorize collection_action_identifier(:widget_sets, :edit, :post) + + widget_params_for_update = widget_params.except('name') + post api_widget_set_url(nil, miq_widget_set), :params => gen_request(:edit, widget_params_for_update) + + expect(response).to have_http_status(:ok) + expect(response.parsed_body['id'].to_i).to eq(miq_widget_set.id) + widget_params["set_data"]["col2"] = [] + widget_params["set_data"]["col3"] = [] + expect(response.parsed_body.values_at(*widget_params_for_update.keys)).to match_array(widget_params_for_update.values) + ws = MiqWidgetSet.find(response.parsed_body['id']) + expect(ws.members.map(&:id)).to eq(widgets.map(&:id)) + end + + it "deletes widget set" do + api_basic_authorize collection_action_identifier(:widget_sets, :delete, :post) + widget_set_id = miq_widget_set.id + group.settings = {"dashboard_order" => [1, 2]} + group.save + post api_widget_set_url(nil, miq_widget_set), :params => gen_request(:delete, widget_params) + expect(response).to have_http_status(:ok) + expect(MiqWidgetSet.find_by(:id => widget_set_id)).to be_nil + group.reload + expect(group.settings["dashboard_order"]).not_to include(widget_set_id) + end + + it "forbids action for non-super-admin user" do + expect_forbidden_request do + post(api_widget_sets_url) + end + end + end + + context "PUT" do + it "updates widget set" do + api_basic_authorize collection_action_identifier(:widget_sets, :edit, :post) + params_for_put = widget_params.except('name') + put api_widget_set_url(nil, miq_widget_set), :params => params_for_put + + expect(response).to have_http_status(:ok) + expect(response.parsed_body['id'].to_i).to eq(miq_widget_set.id) + + widget_params["set_data"]["col2"] = [] + widget_params["set_data"]["col3"] = [] + expect(response.parsed_body.values_at(*params_for_put.keys)).to match_array(params_for_put.values) + end + + it "forbids action for non-super-admin user" do + expect_forbidden_request do + put(api_widget_set_url(nil, miq_widget_set)) + end + end + end + + context "DELETE" do + it "deletes widget set" do + api_basic_authorize collection_action_identifier(:widget_sets, :delete, :post) + widget_set_id = miq_widget_set.id + + delete api_widget_set_url(nil, miq_widget_set) + expect(response).to have_http_status(:no_content) + expect(MiqWidgetSet.find_by(:id => widget_set_id)).to be_nil + end + + it "forbids action for non-super-admin user" do + expect_forbidden_request do + delete(api_widget_set_url(nil, miq_widget_set)) + end + end + end +end