gooddata 2.2.0-java → 2.3.0-java

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.
Files changed (67) hide show
  1. checksums.yaml +5 -5
  2. data/.gdc-ii-config.yaml +42 -1
  3. data/.github/workflows/build.yml +14 -13
  4. data/.github/workflows/pre-merge.yml +13 -13
  5. data/.pronto.yml +1 -0
  6. data/.rubocop.yml +2 -14
  7. data/CHANGELOG.md +9 -0
  8. data/Dockerfile +13 -7
  9. data/Dockerfile.jruby +5 -5
  10. data/Dockerfile.ruby +5 -7
  11. data/Gemfile +4 -2
  12. data/README.md +5 -4
  13. data/Rakefile +1 -1
  14. data/SDK_VERSION +1 -1
  15. data/VERSION +1 -1
  16. data/bin/run_brick.rb +7 -0
  17. data/ci/mysql/pom.xml +6 -1
  18. data/ci/redshift/pom.xml +3 -4
  19. data/docker-compose.lcm.yml +42 -1
  20. data/docker-compose.yml +42 -0
  21. data/gooddata.gemspec +21 -22
  22. data/lcm.rake +9 -0
  23. data/lib/gooddata/bricks/base_pipeline.rb +26 -0
  24. data/lib/gooddata/bricks/brick.rb +0 -1
  25. data/lib/gooddata/bricks/middleware/execution_result_middleware.rb +3 -3
  26. data/lib/gooddata/bricks/pipeline.rb +2 -14
  27. data/lib/gooddata/cloud_resources/mysql/mysql_client.rb +18 -8
  28. data/lib/gooddata/cloud_resources/redshift/drivers/.gitkeepme +0 -0
  29. data/lib/gooddata/cloud_resources/redshift/redshift_client.rb +0 -2
  30. data/lib/gooddata/cloud_resources/snowflake/snowflake_client.rb +1 -1
  31. data/lib/gooddata/lcm/actions/base_action.rb +157 -0
  32. data/lib/gooddata/lcm/actions/collect_data_product.rb +2 -1
  33. data/lib/gooddata/lcm/actions/collect_projects_warning_status.rb +53 -0
  34. data/lib/gooddata/lcm/actions/collect_segment_clients.rb +14 -0
  35. data/lib/gooddata/lcm/actions/initialize_continue_on_error_option.rb +87 -0
  36. data/lib/gooddata/lcm/actions/migrate_gdc_date_dimension.rb +28 -2
  37. data/lib/gooddata/lcm/actions/provision_clients.rb +34 -5
  38. data/lib/gooddata/lcm/actions/synchronize_cas.rb +24 -4
  39. data/lib/gooddata/lcm/actions/synchronize_clients.rb +56 -4
  40. data/lib/gooddata/lcm/actions/synchronize_dataset_mappings.rb +28 -3
  41. data/lib/gooddata/lcm/actions/synchronize_etls_in_segment.rb +48 -11
  42. data/lib/gooddata/lcm/actions/synchronize_kd_dashboard_permission.rb +103 -0
  43. data/lib/gooddata/lcm/actions/synchronize_ldm.rb +60 -15
  44. data/lib/gooddata/lcm/actions/synchronize_ldm_layout.rb +98 -0
  45. data/lib/gooddata/lcm/actions/synchronize_pp_dashboard_permission.rb +108 -0
  46. data/lib/gooddata/lcm/actions/synchronize_schedules.rb +31 -1
  47. data/lib/gooddata/lcm/actions/synchronize_user_filters.rb +14 -9
  48. data/lib/gooddata/lcm/actions/synchronize_user_groups.rb +30 -4
  49. data/lib/gooddata/lcm/actions/synchronize_users.rb +11 -10
  50. data/lib/gooddata/lcm/actions/update_metric_formats.rb +21 -4
  51. data/lib/gooddata/lcm/exceptions/lcm_execution_warning.rb +15 -0
  52. data/lib/gooddata/lcm/helpers/check_helper.rb +19 -0
  53. data/lib/gooddata/lcm/lcm2.rb +45 -4
  54. data/lib/gooddata/lcm/user_bricks_helper.rb +9 -0
  55. data/lib/gooddata/mixins/inspector.rb +1 -1
  56. data/lib/gooddata/models/ldm_layout.rb +38 -0
  57. data/lib/gooddata/models/project.rb +197 -22
  58. data/lib/gooddata/models/project_creator.rb +83 -6
  59. data/lib/gooddata/models/segment.rb +2 -1
  60. data/lib/gooddata/models/user_filters/user_filter_builder.rb +104 -15
  61. data/lib/gooddata/rest/connection.rb +5 -3
  62. data/lib/gooddata/rest/phmap.rb +1 -0
  63. data/lib/gooddata.rb +1 -0
  64. data/lib/gooddata_brick_base.rb +35 -0
  65. data/sonar-project.properties +6 -0
  66. metadata +60 -55
  67. data/lib/gooddata/cloud_resources/redshift/drivers/log4j.properties +0 -15
@@ -35,14 +35,26 @@ module GoodData
35
35
 
36
36
  description 'Localization query'
37
37
  param :localization_query, instance_of(Type::StringType), required: false
38
+
39
+ description 'Abort on error'
40
+ param :abort_on_error, instance_of(Type::StringType), required: false
41
+
42
+ description 'Collect synced status'
43
+ param :collect_synced_status, instance_of(Type::BooleanType), required: false
44
+
45
+ description 'Sync failed list'
46
+ param :sync_failed_list, instance_of(Type::HashType), required: false
38
47
  end
39
48
 
40
49
  RESULT_HEADER = %i[action ok_clients error_clients]
41
50
 
42
51
  class << self
43
52
  def load_metric_data(params)
53
+ collect_synced_status = collect_synced_status(params)
54
+
44
55
  if params&.dig(:input_source, :metric_format) && params[:input_source][:metric_format].present?
45
- metric_input_source = validate_input_source(params[:input_source])
56
+ metric_input_source = validate_input_source(params[:input_source], collect_synced_status)
57
+ return nil unless metric_input_source
46
58
  else
47
59
  return nil
48
60
  end
@@ -68,10 +80,14 @@ module GoodData
68
80
  metrics_hash
69
81
  end
70
82
 
71
- def validate_input_source(input_source)
83
+ def validate_input_source(input_source, continue_on_error)
72
84
  type = input_source[:type] if input_source&.dig(:type)
73
85
  metric_format = input_source[:metric_format]
74
- raise "Incorrect configuration: 'type' of 'input_source' is required" if type.blank?
86
+ if type.blank?
87
+ raise "Incorrect configuration: 'type' of 'input_source' is required" unless continue_on_error
88
+
89
+ return nil
90
+ end
75
91
 
76
92
  modified_input_source = input_source
77
93
  case type
@@ -154,8 +170,9 @@ module GoodData
154
170
  data_product_clients = data_product.clients
155
171
  number_client_ok = 0
156
172
  number_client_error = 0
173
+ collect_synced_status = collect_synced_status(params)
157
174
  metric_group.each do |client_id, formats|
158
- next unless updated_clients.include?(client_id)
175
+ next if !updated_clients.include?(client_id) || (collect_synced_status && sync_failed_client(client_id, params))
159
176
 
160
177
  client = data_product_clients.find { |c| c.id == client_id }
161
178
  begin
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ # (C) 2019-2022 GoodData Corporation
3
+
4
+ module GoodData
5
+ class LcmExecutionWarning < RuntimeError
6
+ DEFAULT_MSG = 'Existing errors during lcm execution'
7
+
8
+ attr_reader :summary_error
9
+
10
+ def initialize(summary_error, message = DEFAULT_MSG)
11
+ super(message)
12
+ @summary_error = summary_error
13
+ end
14
+ end
15
+ end
@@ -10,6 +10,9 @@ module GoodData
10
10
  module LCM2
11
11
  class Helpers
12
12
  class << self
13
+ ABORT_ON_ERROR_PARAM = 'abort_on_error'.to_sym
14
+ COLLECT_SYNCED_STATUS = 'collect_synced_status'.to_sym
15
+
13
16
  def check_params(specification, params)
14
17
  specification.keys.each do |param_name|
15
18
  value = params.send(param_name)
@@ -39,6 +42,22 @@ module GoodData
39
42
  end
40
43
  end
41
44
  end
45
+
46
+ def continue_on_error(params)
47
+ params.include?(ABORT_ON_ERROR_PARAM) && !to_bool(ABORT_ON_ERROR_PARAM, params[ABORT_ON_ERROR_PARAM])
48
+ end
49
+
50
+ def collect_synced_status(params)
51
+ params.include?(COLLECT_SYNCED_STATUS) && to_bool(COLLECT_SYNCED_STATUS, params[COLLECT_SYNCED_STATUS])
52
+ end
53
+
54
+ def to_bool(key, value)
55
+ return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
56
+ return true if value =~ /^(true|t|yes|y|1)$/i
57
+ return false if value == '' || value =~ /^(false|f|no|n|0)$/i
58
+
59
+ raise ArgumentError, "Invalid '#{value}' boolean value for '#{key}' parameter"
60
+ end
42
61
  end
43
62
  end
44
63
  end
@@ -17,6 +17,7 @@ require_relative 'actions/actions'
17
17
  require_relative 'dsl/dsl'
18
18
  require_relative 'helpers/helpers'
19
19
  require_relative 'exceptions/lcm_execution_error'
20
+ require_relative 'exceptions/lcm_execution_warning'
20
21
 
21
22
  using TrueExtensions
22
23
  using FalseExtensions
@@ -97,10 +98,13 @@ module GoodData
97
98
  ImportObjectCollections,
98
99
  SynchronizeComputedAttributes,
99
100
  SynchronizeDataSetMapping,
101
+ SynchronizeLdmLayout,
100
102
  SynchronizeProcesses,
101
103
  SynchronizeSchedules,
102
104
  SynchronizeColorPalette,
103
105
  SynchronizeUserGroups,
106
+ SynchronizePPDashboardPermissions,
107
+ SynchronizeKDDashboardPermissions,
104
108
  SynchronizeNewSegments,
105
109
  UpdateReleaseTable
106
110
  ],
@@ -122,13 +126,19 @@ module GoodData
122
126
  CollectClients,
123
127
  AssociateClients,
124
128
  RenameExistingClientProjects,
129
+ InitializeContinueOnErrorOption,
125
130
  ProvisionClients,
126
131
  UpdateMetricFormats,
127
132
  EnsureTechnicalUsersDomain,
128
133
  EnsureTechnicalUsersProject,
129
134
  CollectDymanicScheduleParams,
130
135
  SynchronizeDataSetMapping,
131
- SynchronizeETLsInSegment
136
+ SynchronizeLdmLayout,
137
+ SynchronizeUserGroups,
138
+ SynchronizePPDashboardPermissions,
139
+ SynchronizeKDDashboardPermissions,
140
+ SynchronizeETLsInSegment,
141
+ CollectProjectsWarningStatus
132
142
  ],
133
143
 
134
144
  rollout: [
@@ -138,14 +148,20 @@ module GoodData
138
148
  CollectSegmentClients,
139
149
  EnsureTechnicalUsersDomain,
140
150
  EnsureTechnicalUsersProject,
151
+ InitializeContinueOnErrorOption,
141
152
  SynchronizeLdm,
142
153
  SynchronizeDataSetMapping,
154
+ SynchronizeLdmLayout,
143
155
  MigrateGdcDateDimension,
144
156
  SynchronizeClients,
157
+ SynchronizeUserGroups,
158
+ SynchronizePPDashboardPermissions,
159
+ SynchronizeKDDashboardPermissions,
145
160
  UpdateMetricFormats,
146
161
  SynchronizeComputedAttributes,
147
162
  CollectDymanicScheduleParams,
148
- SynchronizeETLsInSegment
163
+ SynchronizeETLsInSegment,
164
+ CollectProjectsWarningStatus
149
165
  ],
150
166
 
151
167
  users: [
@@ -340,7 +356,7 @@ module GoodData
340
356
  # Invoke action
341
357
  begin
342
358
  out = run_action action, params
343
- rescue Exception => e # rubocop:disable RescueException
359
+ rescue Exception => e # rubocop:disable Style/RescueException
344
360
  errors << {
345
361
  action: action,
346
362
  err: e,
@@ -362,7 +378,7 @@ module GoodData
362
378
  params.merge!(new_params)
363
379
 
364
380
  # Print action result
365
- print_action_result(action, res)
381
+ print_action_result(action, res) if action.send(:print_result, params)
366
382
 
367
383
  # Store result for final summary
368
384
  results << res
@@ -389,9 +405,28 @@ module GoodData
389
405
  end
390
406
  end
391
407
 
408
+ process_sync_failed_projects(params) if GoodData::LCM2::Helpers.collect_synced_status(params) && strict_mode
409
+
392
410
  result
393
411
  end
394
412
 
413
+ def process_sync_failed_projects(params)
414
+ sync_failed_list = params[:sync_failed_list]
415
+ sync_project_list = sync_failed_list[:project_client_mappings]
416
+ sync_failed_project_list = sync_failed_list[:failed_detailed_projects]
417
+
418
+ if sync_project_list && sync_failed_project_list && sync_project_list.size.positive? && sync_failed_project_list.size.positive?
419
+ failed_project = sync_failed_project_list[0]
420
+ summary_message = "Existing errors during execution. See log for details"
421
+ error_message = failed_project[:message]
422
+ if sync_project_list.size == sync_failed_project_list.size
423
+ raise GoodData::LcmExecutionError.new(summary_message, error_message)
424
+ else
425
+ raise GoodData::LcmExecutionWarning.new(summary_message, error_message)
426
+ end
427
+ end
428
+ end
429
+
395
430
  def run_action(action, params)
396
431
  begin
397
432
  GoodData.gd_logger.start_action action, GoodData.gd_logger
@@ -401,6 +436,12 @@ module GoodData
401
436
  BaseAction.check_params(action.const_get('PARAMS'), params)
402
437
  params.setup_filters(action.const_get('PARAMS'))
403
438
  out = action.send(:call, params)
439
+ rescue Exception => e # rubocop:disable Style/RescueException
440
+ # Log to splunk
441
+ GoodData.gd_logger.error("action=#{action} status=failed message=#{e} exception=#{e.backtrace}")
442
+ # Log to execution log
443
+ GoodData.logger.error("Execution #{action} failed. Error: #{e}. Detail:#{e.backtrace}")
444
+ raise e
404
445
  ensure
405
446
  params.clear_filters
406
447
  GoodData.gd_logger.end_action GoodData.gd_logger
@@ -26,6 +26,15 @@ module GoodData
26
26
 
27
27
  goodot_id.empty? ? client.id : goodot_id
28
28
  end
29
+
30
+ def non_working_clients(domain_clients, working_client_ids)
31
+ non_working_clients = []
32
+ domain_clients.each do |c|
33
+ non_working_clients << c unless working_client_ids.include?(c.client_id.to_s)
34
+ end
35
+
36
+ non_working_clients
37
+ end
29
38
  end
30
39
  end
31
40
  end
@@ -10,7 +10,7 @@ module GoodData
10
10
  module Mixin
11
11
  # When an RSpec test like this fails,
12
12
  #
13
- # @my_array.should == [@some_model, @some_model2]
13
+ # expect(@my_array).to == [@some_model, @some_model2]
14
14
  #
15
15
  # RSpec will call inspect on each of the objects to "help" you figure out
16
16
  # what went wrong. Well, inspect will usually dump a TON OF SHIT and make trying
@@ -0,0 +1,38 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+ #
4
+ # Copyright (c) 2022 GoodData Corporation. All rights reserved.
5
+ # This source code is licensed under the BSD-style license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+
8
+ module GoodData
9
+ class LdmLayout
10
+ DEFAULT_EMPTY_LDM_LAYOUT = {
11
+ "ldmLayout" => {
12
+ "layout" => []
13
+ }
14
+ }
15
+
16
+ LDM_LAYOUT_URI = '/gdc/dataload/internal/projects/%<project_id>s/ldmLayout'
17
+
18
+ class << self
19
+ def get(opts = { :client => GoodData.connection, :project => GoodData.project })
20
+ client, project = GoodData.get_client_and_project(opts)
21
+ get_uri = LDM_LAYOUT_URI % { project_id: project.pid }
22
+
23
+ client.get(get_uri)
24
+ end
25
+ end
26
+
27
+ def initialize(data)
28
+ @data = data
29
+ end
30
+
31
+ def save(opts)
32
+ client, project = GoodData.get_client_and_project(opts)
33
+ post_uri = LDM_LAYOUT_URI % { project_id: project.pid }
34
+
35
+ client.post(post_uri, @data, opts)
36
+ end
37
+ end
38
+ end
@@ -31,6 +31,7 @@ require_relative 'project_log_formatter'
31
31
  require_relative 'project_role'
32
32
  require_relative 'blueprint/blueprint'
33
33
  require_relative 'dataset_mapping'
34
+ require_relative 'ldm_layout'
34
35
 
35
36
  require_relative 'metadata/scheduled_mail'
36
37
  require_relative 'metadata/scheduled_mail/dashboard_attachment'
@@ -272,6 +273,29 @@ module GoodData
272
273
  }
273
274
  end
274
275
 
276
+ def get_ldm_layout(from_project)
277
+ GoodData::LdmLayout.get(:client => from_project.client, :project => from_project)
278
+ rescue StandardError => e
279
+ GoodData.logger.warn "An unexpected error when get ldm layout. Error: #{e.message}"
280
+ GoodData::LdmLayout::DEFAULT_EMPTY_LDM_LAYOUT
281
+ end
282
+
283
+ def save_ldm_layout(ldm_layout_json, to_project)
284
+ ldm_layout = GoodData::LdmLayout.new(ldm_layout_json)
285
+ begin
286
+ ldm_layout.save(:client => to_project.client, :project => to_project)
287
+ status = "OK"
288
+ rescue StandardError => e
289
+ GoodData.logger.warn "An unexpected error when save ldm layout. Error: #{e.message}"
290
+ status = "Failed"
291
+ end
292
+
293
+ {
294
+ to: to_project.pid,
295
+ status: status
296
+ }
297
+ end
298
+
275
299
  # @param from_project The source project
276
300
  # @param to_project The target project
277
301
  # @param options Optional parameters
@@ -416,16 +440,6 @@ module GoodData
416
440
  new_group.project = to_project
417
441
  new_group.description = ug.description
418
442
  new_group.save
419
- # migrate dashboard "grantees"
420
- dashboards = from_project.dashboards
421
- dashboards.each do |dashboard|
422
- new_dashboard = to_project.dashboards.select { |dash| dash.title == dashboard.title }.first
423
- next unless new_dashboard
424
- grantee = dashboard.grantees['granteeURIs']['items'].select { |item| item['aclEntryURI']['grantee'].split('/').last == ug.links['self'].split('/').last }.first
425
- next unless grantee
426
- permission = grantee['aclEntryURI']['permission']
427
- new_dashboard.grant(:member => new_group, :permission => permission)
428
- end
429
443
 
430
444
  {
431
445
  from: from_project.pid,
@@ -436,6 +450,68 @@ module GoodData
436
450
  end
437
451
  end
438
452
 
453
+ def transfer_dashboard_permission(from_project, to_project, source_dashboards, target_dashboards)
454
+ source_user_groups = from_project.user_groups
455
+ target_user_groups = to_project.user_groups
456
+
457
+ source_dashboards.each do |source_dashboard|
458
+ target_dashboard = target_dashboards.select { |dash| dash.title == source_dashboard.title }.first
459
+ next unless target_dashboard
460
+
461
+ begin
462
+ source_group_dashboards = dashboard_user_groups(source_user_groups, source_dashboard)
463
+ target_group_dashboards = dashboard_user_groups(target_user_groups, target_dashboard)
464
+
465
+ common_group_names = source_group_dashboards.flat_map { |s| target_group_dashboards.select { |t| t[:name] == s[:name] } }.map { |x| [x[:name], true] }.to_h
466
+
467
+ remove_user_groups_from_dashboard(target_group_dashboards, target_dashboard, common_group_names)
468
+ add_user_groups_to_dashboard(source_group_dashboards, target_dashboard, common_group_names, target_user_groups)
469
+ rescue StandardError => e
470
+ GoodData.logger.warn "Failed to synchronize dashboard permission from project: '#{from_project.title}', PID: '#{from_project.pid}' to project: '#{to_project.title}', PID: '#{to_project.pid}', dashboard: '#{target_dashboard.title}', dashboard uri: '#{target_dashboard.uri}' . Error: #{e.message} - #{e}" # rubocop:disable Metrics/LineLength
471
+ end
472
+ end
473
+ end
474
+
475
+ def dashboard_user_groups(user_groups, dashboard)
476
+ group_dashboards = []
477
+ dashboard_grantees = dashboard.grantees['granteeURIs']['items'].select { |item| item['aclEntryURI']['grantee'].include?('/usergroups/') }
478
+
479
+ dashboard_grantees.each do |dashboard_grantee|
480
+ permission = dashboard_grantee['aclEntryURI']['permission']
481
+ group_id = dashboard_grantee['aclEntryURI']['grantee'].split('/').last
482
+ user_group = user_groups.select { |group| group.links['self'].split('/').last == group_id }.first
483
+ next unless user_group
484
+
485
+ group_dashboards << {
486
+ name: user_group.name,
487
+ user_group: user_group,
488
+ permission: permission
489
+ }
490
+ end
491
+ group_dashboards
492
+ end
493
+
494
+ def remove_user_groups_from_dashboard(group_dashboards, dashboard, common_group_names)
495
+ group_dashboards.each do |group_dashboard|
496
+ group_name = group_dashboard[:name]
497
+ next if common_group_names && common_group_names[group_name]
498
+
499
+ dashboard.revoke(:member => group_dashboard[:user_group], :permission => group_dashboard[:permission])
500
+ end
501
+ end
502
+
503
+ def add_user_groups_to_dashboard(group_dashboards, dashboard, common_group_names, target_user_groups)
504
+ group_dashboards.each do |group_dashboard|
505
+ group_name = group_dashboard[:name]
506
+ next if common_group_names && common_group_names[group_name]
507
+
508
+ target_user_group = target_user_groups.select { |group| group.name == group_name }.first
509
+ next unless target_user_group
510
+
511
+ dashboard.grant(:member => target_user_group, :permission => group_dashboard[:permission])
512
+ end
513
+ end
514
+
439
515
  # Clones project along with etl and schedules.
440
516
  #
441
517
  # @param client [GoodData::Rest::Client] GoodData client to be used for connection
@@ -443,7 +519,7 @@ module GoodData
443
519
  # Object to be cloned from. Can be either segment in which case we take
444
520
  # the master, client in which case we take its project, string in which
445
521
  # case we treat is as an project object or directly project.
446
- def transfer_schedules(from_project, to_project)
522
+ def transfer_schedules(from_project, to_project, has_cycle_trigger = false)
447
523
  to_project_processes = to_project.processes.sort_by(&:name)
448
524
  from_project_processes = from_project.processes.sort_by(&:name)
449
525
  from_project_processes.reject!(&:add_v2_component?)
@@ -496,15 +572,23 @@ module GoodData
496
572
  end
497
573
 
498
574
  results = []
575
+ update_trigger_schedules = []
499
576
  loop do # rubocop:disable Metrics/BlockLength
500
577
  break if stack.empty?
501
578
  state, changed_schedule = stack.shift
579
+ lazy_update_trigger_info = false
502
580
  if state == :added
503
581
  schedule_spec = changed_schedule
504
582
  if schedule_spec[:after] && !schedule_cache[schedule_spec[:after]]
505
- stack << [state, schedule_spec]
506
- next
583
+ if has_cycle_trigger
584
+ # The schedule is triggered by another schedule
585
+ lazy_update_trigger_info = true
586
+ else
587
+ stack << [state, schedule_spec]
588
+ next
589
+ end
507
590
  end
591
+
508
592
  remote_process, process_spec = cache.find do |_remote, local, schedule|
509
593
  (schedule_spec[:process_id] == local.process_id) && (schedule.name == schedule_spec[:name])
510
594
  end
@@ -517,8 +601,21 @@ module GoodData
517
601
  if process_spec.type != :dataload
518
602
  executable = schedule_spec[:executable] || (process_spec.type == :ruby ? 'main.rb' : 'main.grf')
519
603
  end
604
+
520
605
  params = schedule_parameters(schedule_spec)
521
- created_schedule = remote_process.create_schedule(schedule_spec[:cron] || schedule_cache[schedule_spec[:after]], executable, params)
606
+
607
+ if lazy_update_trigger_info
608
+ # Temporary update nil for trigger info. The trigger info will be update late after transfer all schedules
609
+ created_schedule = remote_process.create_schedule(nil, executable, params)
610
+ update_trigger_schedules << {
611
+ state: :added,
612
+ schedule: created_schedule,
613
+ after: schedule_spec[:after]
614
+ }
615
+ else
616
+ created_schedule = remote_process.create_schedule(schedule_spec[:cron] || schedule_cache[schedule_spec[:after]], executable, params)
617
+ end
618
+
522
619
  schedule_cache[created_schedule.name] = created_schedule
523
620
 
524
621
  results << {
@@ -529,8 +626,13 @@ module GoodData
529
626
  else
530
627
  schedule_spec = changed_schedule[:new_obj]
531
628
  if schedule_spec[:after] && !schedule_cache[schedule_spec[:after]]
532
- stack << [state, schedule_spec]
533
- next
629
+ if has_cycle_trigger
630
+ # The schedule is triggered by another schedule
631
+ lazy_update_trigger_info = true
632
+ else
633
+ stack << [state, schedule_spec]
634
+ next
635
+ end
534
636
  end
535
637
 
536
638
  remote_process, process_spec = cache.find do |i|
@@ -543,8 +645,12 @@ module GoodData
543
645
 
544
646
  schedule.params = (schedule_spec[:params] || {})
545
647
  schedule.cron = schedule_spec[:cron] if schedule_spec[:cron]
546
- schedule.after = schedule_cache[schedule_spec[:after]] if schedule_spec[:after]
547
- schedule.trigger_execution_status = schedule_cache[schedule_spec[:trigger_execution_status]] if schedule_spec[:after]
648
+
649
+ unless lazy_update_trigger_info
650
+ schedule.after = schedule_cache[schedule_spec[:after]] if schedule_spec[:after]
651
+ schedule.trigger_execution_status = schedule_cache[schedule_spec[:trigger_execution_status]] if schedule_spec[:after]
652
+ end
653
+
548
654
  schedule.hidden_params = schedule_spec[:hidden_params] || {}
549
655
  if process_spec.type != :dataload
550
656
  schedule.executable = schedule_spec[:executable] || (process_spec.type == :ruby ? 'main.rb' : 'main.grf')
@@ -556,6 +662,15 @@ module GoodData
556
662
  schedule.save
557
663
  schedule_cache[schedule.name] = schedule
558
664
 
665
+ if lazy_update_trigger_info
666
+ update_trigger_schedules << {
667
+ state: :changed,
668
+ schedule: schedule,
669
+ after: schedule_spec[:after],
670
+ trigger_execution_status: schedule_spec[:trigger_execution_status]
671
+ }
672
+ end
673
+
559
674
  results << {
560
675
  state: :changed,
561
676
  process: remote_process,
@@ -564,6 +679,22 @@ module GoodData
564
679
  end
565
680
  end
566
681
 
682
+ if has_cycle_trigger
683
+ update_trigger_schedules.each do |update_trigger_schedule|
684
+ working_schedule = update_trigger_schedule[:schedule]
685
+ working_schedule.after = schedule_cache[update_trigger_schedule[:after]]
686
+ working_schedule.trigger_execution_status = schedule_cache[update_trigger_schedule[:trigger_execution_status]] if update_trigger_schedule[:state] == :changed
687
+
688
+ # Update trigger info
689
+ working_schedule.save
690
+
691
+ # Update transfer result
692
+ results.each do |transfer_result|
693
+ transfer_result[:schedule] = working_schedule if transfer_result[:schedule].obj_id == working_schedule.obj_id
694
+ end
695
+ end
696
+ end
697
+
567
698
  diff[:removed].each do |removed_schedule|
568
699
  GoodData.logger.info("Removing schedule #{removed_schedule[:name]}")
569
700
 
@@ -871,6 +1002,14 @@ module GoodData
871
1002
  GoodData::Dashboard[id, project: self, client: client]
872
1003
  end
873
1004
 
1005
+ # Helper for getting analytical dashboards (KD dashboards) of a project
1006
+ #
1007
+ # @param id [String | Number | Object] Anything that you can pass to GoodData::Dashboard[id]
1008
+ # @return [GoodData::AnalyticalDashboard | Array<GoodData::AnalyticalDashboard>] dashboard instance or list
1009
+ def analytical_dashboards(id = :all)
1010
+ GoodData::AnalyticalDashboard[id, project: self, client: client]
1011
+ end
1012
+
874
1013
  def data_permissions(id = :all)
875
1014
  GoodData::MandatoryUserFilter[id, client: client, project: self]
876
1015
  end
@@ -1797,15 +1936,15 @@ module GoodData
1797
1936
  end
1798
1937
 
1799
1938
  # reassign to groups
1939
+ removal_user_group_members = []
1800
1940
  mappings = new_users.map(&:to_hash).flat_map do |user|
1941
+ removal_user_group_members << user[:login] if user[:user_group]&.empty?
1801
1942
  groups = user[:user_group] || []
1802
1943
  groups.map { |g| [user[:login], g] }
1803
1944
  end
1945
+
1804
1946
  unless mappings.empty?
1805
- users_lookup = users.reduce({}) do |a, e|
1806
- a[e.login] = e
1807
- a
1808
- end
1947
+ users_lookup = login_users
1809
1948
  mappings.group_by { |_, g| g }.each do |g, mapping|
1810
1949
  remote_users = mapping.map { |user, _| user }.map { |login| users_lookup[login] && users_lookup[login].uri }.reject(&:nil?)
1811
1950
  GoodData.logger.info("Assigning users #{remote_users} to group #{g}")
@@ -1819,14 +1958,42 @@ module GoodData
1819
1958
  end
1820
1959
  mentioned_groups = mappings.map(&:last).uniq
1821
1960
  groups_to_cleanup = user_groups_cache.reject { |g| mentioned_groups.include?(g.name) }
1961
+
1822
1962
  # clean all groups not mentioned with exception of whitelisted users
1823
1963
  groups_to_cleanup.each do |g|
1824
1964
  g.set_members(whitelist_users(g.members.map(&:to_hash), [], options[:whitelists], :include).first.map { |x| x[:uri] })
1825
1965
  end
1826
1966
  end
1967
+
1968
+ remove_member_from_group(users_lookup, removal_user_group_members, user_groups_cache)
1827
1969
  GoodData::Helpers.join(results, diff_results, [:user], [:login_uri])
1828
1970
  end
1829
1971
 
1972
+ def remove_member_from_group(users_lookup, removal_user_group_members, user_groups_cache)
1973
+ unless removal_user_group_members.empty?
1974
+ users_lookup ||= login_users
1975
+ current_user_groups = user_groups_cache || user_groups
1976
+ removal_user_group_members.uniq.each do |login|
1977
+ user_uri = users_lookup[login]&.uri
1978
+
1979
+ # remove user from group if exists as group member
1980
+ current_user_groups.each do |user_group|
1981
+ if user_group.member?(user_uri)
1982
+ GoodData.logger.info("Removing #{user_uri} user from group #{user_group.name}")
1983
+ user_group.remove_members(user_uri)
1984
+ end
1985
+ end
1986
+ end
1987
+ end
1988
+ end
1989
+
1990
+ def login_users
1991
+ users.reduce({}) do |a, e|
1992
+ a[e.login] = e
1993
+ a
1994
+ end
1995
+ end
1996
+
1830
1997
  def disable_users(list, options = {})
1831
1998
  list = list.map(&:to_hash)
1832
1999
  url = "#{uri}/users"
@@ -2047,6 +2214,14 @@ module GoodData
2047
2214
  GoodData::Project.update_dataset_mapping(model_mapping_json, self)
2048
2215
  end
2049
2216
 
2217
+ def ldm_layout
2218
+ GoodData::Project.get_ldm_layout(self)
2219
+ end
2220
+
2221
+ def save_ldm_layout(ldm_layout_json)
2222
+ GoodData::Project.save_ldm_layout(ldm_layout_json, self)
2223
+ end
2224
+
2050
2225
  def transfer_processes(target)
2051
2226
  GoodData::Project.transfer_processes(self, target)
2052
2227
  end