Use state_machine to manage jira_import state.

This commit is contained in:
Pavel Balashou
2026-02-10 11:39:14 +01:00
parent b8acf1e39d
commit 2e57030482
24 changed files with 350 additions and 135 deletions
+3
View File
@@ -167,6 +167,9 @@ gem "meta-tags", "~> 2.22.2"
gem "paper_trail", "~> 17.0.0"
# State machine with audit trail
gem "statesman", "~> 13.1.0"
gem "op-clamav-client", "~> 3.4", require: "clamav"
# Global ID for polymorphic associations
+3
View File
@@ -1405,6 +1405,7 @@ GEM
sprockets (>= 3.0.0)
ssrf_filter (1.0.8)
stackprof (0.2.27)
statesman (13.1.0)
store_attribute (2.0.1)
activerecord (>= 6.1)
stringex (2.8.6)
@@ -1727,6 +1728,7 @@ DEPENDENCIES
sprockets (~> 3.7.2)
sprockets-rails (~> 3.5.1)
stackprof
statesman (~> 13.1.0)
store_attribute (~> 2.0)
stringex (~> 2.8.5)
structured_warnings (~> 0.5.0)
@@ -2226,6 +2228,7 @@ CHECKSUMS
sprockets-rails (3.5.2) sha256=a9e88e6ce9f8c912d349aa5401509165ec42326baf9e942a85de4b76dbc4119e
ssrf_filter (1.0.8) sha256=03f49f54837e407d43ee93ec733a8a94dc1bcf8185647ac61606e63aaedaa0db
stackprof (0.2.27) sha256=aff6d28656c852e74cf632cc2046f849033dc1dedffe7cb8c030d61b5745e80c
statesman (13.1.0) sha256=3ecb78466dfd2682e433f335a7722aa0e5b8c6853d72d83e460151b8af17a84e
store_attribute (2.0.1) sha256=643655e4800655b58379e8b01bd524f5586093a9d88698483ac8762cda25b5ab
stringex (2.8.6) sha256=c7b382d2b2a47a1e1646f256df201c48d487d6296fbb289d76802f67f5e929c4
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
@@ -42,7 +42,7 @@ module Admin::Import::Jira::ImportRuns
end
def status
render(Admin::Import::Jira::ImportRuns::StatusBadgeComponent.new(model.status))
render(Admin::Import::Jira::ImportRuns::StatusBadgeComponent.new(model.current_state))
end
def last_changed
@@ -41,13 +41,18 @@ module Admin::Import::Jira::ImportRuns
def status_color_scheme(status)
case status
when JiraImport::IMPORT_ERROR, JiraImport::REVERT_ERROR,
JiraImport::INSTANCE_META_ERROR, JiraImport::PROJECTS_META_ERROR
when JiraImportStateMachine::IMPORT_ERROR,
JiraImportStateMachine::REVERT_ERROR,
JiraImportStateMachine::INSTANCE_META_ERROR,
JiraImportStateMachine::PROJECTS_META_ERROR
:danger
when JiraImport::COMPLETED, JiraImport::REVERTED
when JiraImportStateMachine::COMPLETED,
JiraImportStateMachine::REVERTED
:success
when JiraImport::INSTANCE_META_FETCHING, JiraImport::PROJECTS_META_FETCHING,
JiraImport::IMPORTING, JiraImport::REVERTING
when JiraImportStateMachine::INSTANCE_META_FETCHING,
JiraImportStateMachine::PROJECTS_META_FETCHING,
JiraImportStateMachine::IMPORTING,
JiraImportStateMachine::REVERTING
:accent
else
:attention
@@ -32,17 +32,17 @@ See COPYRIGHT and LICENSE files for more details.
concat(render(Primer::Beta::Text.new(font_weight: :semibold, tag: :div)) {
I18n.t(:"admin.jira.run.wizard.sections.confirm_import.title")
})
if model.status_before?(JiraImport::PROJECTS_META_DONE)
if model.status_before?(JiraImportStateMachine::PROJECTS_META_DONE)
concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) {
I18n.t(:"admin.jira.run.wizard.sections.confirm_import.caption")
})
elsif model.status_equal_or_after?(JiraImport::IMPORTED)
elsif model.status_equal_or_after?(JiraImportStateMachine::IMPORTED)
concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) {
I18n.t(:"admin.jira.run.wizard.sections.confirm_import.caption_done")
})
end
end
if model.status?(JiraImport::PROJECTS_META_DONE, JiraImport::IMPORT_ERROR)
if model.in_state?(JiraImportStateMachine::PROJECTS_META_DONE, JiraImportStateMachine::IMPORT_ERROR)
box.with_row(mb: 3) do
render(Primer::Beta::Text.new) {
I18n.t(:"admin.jira.run.wizard.sections.confirm_import.description")
@@ -54,7 +54,7 @@ See COPYRIGHT and LICENSE files for more details.
list: import_selection
))
end
if model.status?(JiraImport::PROJECTS_META_DONE)
if model.in_state?(JiraImportStateMachine::PROJECTS_META_DONE)
box.with_row do
render(Primer::Beta::Button.new(
scheme: :primary,
@@ -66,12 +66,12 @@ See COPYRIGHT and LICENSE files for more details.
I18n.t(:"admin.jira.run.wizard.sections.confirm_import.button_start")
}
end
elsif model.status?(JiraImport::IMPORT_ERROR)
elsif model.in_state?(JiraImportStateMachine::IMPORT_ERROR)
box.with_row do
render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, 'import'))
end
end
elsif model.status?(JiraImport::IMPORTING)
elsif model.in_state?(JiraImportStateMachine::IMPORTING)
box.with_row(mt: 3, mb: 3) do
render(Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new(
I18n.t(:"admin.jira.run.wizard.sections.confirm_import.label_progress")
@@ -34,13 +34,13 @@ See COPYRIGHT and LICENSE files for more details.
concat(render(Primer::Beta::Text.new(font_weight: :semibold, tag: :div)) {
I18n.t(:"admin.jira.run.wizard.sections.fetch_data.title")
})
if model.status_equal_or_after?(JiraImport::INSTANCE_META_DONE)
if model.status_equal_or_after?(JiraImportStateMachine::INSTANCE_META_DONE)
concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) {
I18n.t(:"admin.jira.run.wizard.sections.fetch_data.caption_done")
})
end
end
if model.status?(JiraImport::INSTANCE_META_DONE, JiraImport::CONFIGURING)
if model.in_state?(:instance_meta_done, :configuring)
row.with_column do
render(
Primer::Beta::IconButton.new(
@@ -56,7 +56,7 @@ See COPYRIGHT and LICENSE files for more details.
end
end
end
if model.status?(JiraImport::INITIAL)
if model.in_state?(JiraImportStateMachine::INITIAL)
box.with_row(mb: 3) do
render(Primer::Beta::Text.new) {
I18n.t(:"admin.jira.run.wizard.sections.fetch_data.description")
@@ -73,11 +73,11 @@ See COPYRIGHT and LICENSE files for more details.
I18n.t(:"admin.jira.run.wizard.sections.fetch_data.button_fetch")
end
end
elsif model.status?(JiraImport::INSTANCE_META_ERROR)
elsif model.in_state?(:instance_meta_error)
box.with_row(mt: 3) do
render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, 'fetch'))
end
elsif model.status?(JiraImport::INSTANCE_META_FETCHING)
elsif model.in_state?(:instance_meta_fetching)
box.with_row(mt: 3) do
render(Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new(
I18n.t(:"admin.jira.run.wizard.sections.fetch_data.label_progress")
@@ -32,17 +32,17 @@ See COPYRIGHT and LICENSE files for more details.
concat(render(Primer::Beta::Text.new(font_weight: :semibold, tag: :div)) {
I18n.t(:"admin.jira.run.wizard.sections.import_scope.title")
})
if model.status_before?(JiraImport::INSTANCE_META_DONE)
if model.status_before?(JiraImportStateMachine::INSTANCE_META_DONE)
concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) {
I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption")
})
elsif model.status_equal_or_after?(JiraImport::PROJECTS_META_DONE)
elsif model.status_equal_or_after?(JiraImportStateMachine::PROJECTS_META_DONE)
concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) {
I18n.t(:"admin.jira.run.wizard.sections.import_scope.caption_done")
})
end
end
if model.status?(JiraImport::INSTANCE_META_DONE)
if model.in_state?(JiraImportStateMachine::INSTANCE_META_DONE)
box.with_row(mb: 3, mt: 3) do
render(Primer::Alpha::Banner.new(scheme: :warning, icon: :alert)) {
I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_info")
@@ -82,7 +82,7 @@ See COPYRIGHT and LICENSE files for more details.
I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_continue")
}
end
elsif model.status?(JiraImport::CONFIGURING, JiraImport::PROJECTS_META_ERROR)
elsif model.in_state?(JiraImportStateMachine::CONFIGURING, JiraImportStateMachine::PROJECTS_META_ERROR)
box.with_row(mb: 3, mt: 3) do
render(Primer::Beta::Text.new) {
I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_import")
@@ -99,7 +99,7 @@ See COPYRIGHT and LICENSE files for more details.
I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_select")
end
end
if model.status?(JiraImport::CONFIGURING)
if model.in_state?(JiraImportStateMachine::CONFIGURING)
box.with_row do
render(Primer::Beta::Button.new(
scheme: :primary,
@@ -112,12 +112,12 @@ See COPYRIGHT and LICENSE files for more details.
I18n.t(:"admin.jira.run.wizard.sections.import_scope.button_continue")
}
end
elsif model.status?(JiraImport::PROJECTS_META_ERROR)
elsif model.in_state?(JiraImportStateMachine::PROJECTS_META_ERROR)
box.with_row do
render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, 'stats'))
end
end
elsif model.status?(JiraImport::PROJECTS_META_FETCHING)
elsif model.in_state?(JiraImportStateMachine::PROJECTS_META_FETCHING)
box.with_row(mt: 3) do
render(Admin::Import::Jira::ImportRuns::ProgressBoxComponent.new(
I18n.t(:"admin.jira.run.wizard.sections.import_scope.label_progress")
@@ -28,14 +28,14 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%= flex_layout do |box|
if model.status?(JiraImport::REVERTING, JiraImport::REVERTED, JiraImport::REVERT_ERROR)
if model.in_state?(JiraImportStateMachine::REVERTING, JiraImportStateMachine::REVERTED, JiraImportStateMachine::REVERT_ERROR)
box.with_row do
concat(render(Primer::Beta::Text.new(font_weight: :semibold, tag: :div)) {
I18n.t(:"admin.jira.run.wizard.sections.import_result.label_revert")
})
end
case model.status
when JiraImport::REVERTING
case model.current_state
when JiraImportStateMachine::REVERTING
box.with_row(mt: 3) do
render(Primer::Box.new(border: true, border_color: :accent, border_radius: 2, bg: :accent, p: 3)) do
concat render(Primer::Beta::Spinner.new(size: :small))
@@ -44,13 +44,13 @@ See COPYRIGHT and LICENSE files for more details.
}
end
end
when JiraImport::REVERTED
when JiraImportStateMachine::REVERTED
box.with_row(mt: 3) do
render(Primer::Alpha::Banner.new(scheme: :success, icon: :"check-circle")) {
I18n.t(:"admin.jira.run.wizard.sections.import_result.label_reverted")
}
end
when JiraImport::REVERT_ERROR
when JiraImportStateMachine::REVERT_ERROR
box.with_row(mt: 3) do
render(Admin::Import::Jira::ImportRuns::ErrorBannerComponent.new(model, 'revert'))
end
@@ -60,20 +60,20 @@ See COPYRIGHT and LICENSE files for more details.
concat(render(Primer::Beta::Text.new(font_weight: :semibold, tag: :div)) {
I18n.t(:"admin.jira.run.wizard.sections.import_result.title")
})
if model.status_before?(JiraImport::IMPORTED)
if model.status_before?(JiraImportStateMachine::IMPORTED)
concat(render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) {
I18n.t(:"admin.jira.run.wizard.sections.import_result.caption")
})
end
end
if model.status?(JiraImport::IMPORTED)
if model.in_state?(JiraImportStateMachine::IMPORTED)
box.with_row(mt: 3) do
render(Primer::Alpha::Banner.new(scheme: :success, icon: :"check-circle")) {
I18n.t(:"admin.jira.run.wizard.sections.import_result.info")
}
end
end
if model.status?(JiraImport::IMPORTED, JiraImport::COMPLETED)
if model.in_state?(JiraImportStateMachine::IMPORTED, JiraImportStateMachine::COMPLETED)
box.with_row(mt: 3, mb: 3) do
render(Admin::Import::Jira::ImportRuns::InfoListBoxComponent.new(
title: I18n.t(:"admin.jira.run.wizard.sections.import_result.label_results"),
@@ -81,7 +81,7 @@ See COPYRIGHT and LICENSE files for more details.
))
end
end
if model.status?(JiraImport::IMPORTED)
if model.in_state?(JiraImportStateMachine::IMPORTED)
box.with_row(mb: 3) do
render(Primer::Alpha::Banner.new(scheme: :default, icon: :info)) {
I18n.t(:"admin.jira.run.wizard.sections.import_result.preview_description")
@@ -48,13 +48,13 @@ module Admin::Import::Jira
menu_item :jira_import
before_action :require_admin
before_action :find_jira_and_jira_import, only: %i[show continue remove revert_modal]
before_action :find_jira_and_jira_import, only: %i[show continue remove revert_modal history]
def show; end
def new
jira = Jira.find(params[:jira_id])
jira_import = JiraImport.create!(author_id: current_user.id, jira_id: jira.id, status: JiraImport::INITIAL)
jira_import = JiraImport.create!(author_id: current_user.id, jira_id: jira.id)
redirect_to(admin_import_jira_run_path(jira_id: jira.id, id: jira_import.id))
end
@@ -76,6 +76,10 @@ module Admin::Import::Jira
redirect_to admin_import_jira_path(@jira), status: :see_other
end
def history
@history = @jira_import.history
end
private
def change_step(step)
@@ -90,7 +94,7 @@ module Admin::Import::Jira
def handle_error(error)
respond_to do |format|
format.turbo_stream do
render_error_flash_message_via_turbo_stream(message: error.message)
render_error_flash_message_via_turbo_stream(message: "#{error.message}\n#{error.backtrace}")
respond_with_turbo_streams
end
format.html do
@@ -101,49 +105,31 @@ module Admin::Import::Jira
end
def init
return unless @jira_import.status_equal_or_before?(JiraImport::CONFIGURING)
@jira_import.update!(status: JiraImport::INITIAL)
@jira_import.transition_to!(:initial)
end
def fetch_instance_meta
return unless @jira_import.status_equal_or_before?(JiraImport::CONFIGURING)
job = JiraInstanceMetaDataJob.perform_later(@jira_import.id)
@jira_import.update!(status: JiraImport::INSTANCE_META_FETCHING, job_id: job.job_id)
@jira_import.transition_to!(:instance_meta_fetching, job_id: "JOOOOOOOO")
end
def fetch_projects_meta
return unless @jira_import.status?(JiraImport::CONFIGURING, JiraImport::PROJECTS_META_ERROR)
job = JiraProjectsMetaDataJob.perform_later(@jira_import.id)
@jira_import.update!(status: JiraImport::PROJECTS_META_FETCHING, job_id: job.job_id)
@jira_import.transition_to!(:projects_meta_fetching)
end
def import
return unless @jira_import.status?(JiraImport::IMPORT_ERROR, JiraImport::PROJECTS_META_DONE)
job = JiraFetchAndImportProjectsJob.perform_later(@jira_import.id)
@jira_import.update!(status: JiraImport::IMPORTING, job_id: job.job_id)
@jira_import.transition_to!(:importing)
end
def configure
return unless @jira_import.status?(JiraImport::INSTANCE_META_DONE)
@jira_import.update!(status: JiraImport::CONFIGURING)
@jira_import.transition_to!(:configuring)
end
def revert
return unless @jira_import.status?(JiraImport::REVERT_ERROR, JiraImport::IMPORTED)
job = JiraRevertJiraImportJob.perform_later(@jira_import.id)
@jira_import.update!(status: JiraImport::REVERTING, job_id: job.job_id)
@jira_import.transition_to!(:reverting)
end
def finalize
return unless @jira_import.status?(JiraImport::IMPORTED)
@jira_import.update!(status: JiraImport::COMPLETED)
@jira_import.transition_to!(:completed)
end
def find_jira_and_jira_import
@@ -43,7 +43,7 @@ module Admin::Import::Jira::ImportRuns
method: "morph"
)
update_via_turbo_stream(
component: ::Admin::Import::Jira::ImportRuns::StreamableStatusBadgeComponent.new(@jira_import.status),
component: ::Admin::Import::Jira::ImportRuns::StreamableStatusBadgeComponent.new(@jira_import.current_state),
method: "morph"
)
render turbo_stream: turbo_streams
+22 -63
View File
@@ -32,72 +32,31 @@ class JiraImport < ApplicationRecord
belongs_to :jira
belongs_to :author, class_name: "User"
INITIAL = "initial"
INSTANCE_META_FETCHING = "instance-meta-fetching"
INSTANCE_META_ERROR = "instance-meta-error"
INSTANCE_META_DONE = "instance-meta-done"
CONFIGURING = "configuring"
PROJECTS_META_FETCHING = "projects-meta-fetching"
PROJECTS_META_ERROR = "projects-meta-error"
PROJECTS_META_DONE = "projects-meta-done"
IMPORTING = "importing"
IMPORT_ERROR = "import-error"
IMPORTED = "imported"
COMPLETED = "completed"
REVERTING = "reverting"
REVERT_ERROR = "revert-error"
REVERTED = "reverted"
has_many :transitions, class_name: "JiraImportTransition", autosave: false
STATUSES = [
INITIAL,
INSTANCE_META_FETCHING,
INSTANCE_META_ERROR,
INSTANCE_META_DONE,
CONFIGURING,
PROJECTS_META_FETCHING,
PROJECTS_META_ERROR,
PROJECTS_META_DONE,
IMPORTING,
IMPORT_ERROR,
IMPORTED,
COMPLETED,
REVERTING,
REVERT_ERROR,
REVERTED
].freeze
def status_equal_or_after?(check_status)
STATUSES.index(status) >= STATUSES.index(check_status)
def state_machine
@state_machine ||= JiraImportStateMachine.new(
self,
transition_class: JiraImportTransition,
association_name: :transitions
)
end
def status_equal_or_before?(check_status)
STATUSES.index(status) <= STATUSES.index(check_status)
end
def status_before?(check_status)
STATUSES.index(status) < STATUSES.index(check_status)
end
def status_after?(check_status)
STATUSES.index(status) > STATUSES.index(check_status)
end
def status?(*check_statuses)
check_statuses.include?(status)
end
def deletable?
!status_running? && !status?(IMPORTED, IMPORT_ERROR, REVERT_ERROR)
end
def status_running?
[
INSTANCE_META_FETCHING,
PROJECTS_META_FETCHING,
IMPORTING,
REVERTING
].include?(status)
end
delegate :can_transition_to?,
:current_state,
:history,
:last_transition,
:last_transition_to,
:transition_to!,
:transition_to,
:in_state?,
:status_running?,
:status_equal_or_after?,
:status_equal_or_before?,
:status_after?,
:status_before?,
:deletable?,
to: :state_machine
def project_ids
(projects || []).pluck("id")
+95
View File
@@ -0,0 +1,95 @@
class JiraImportStateMachine
include Statesman::Machine
state :initial, initial: true
state :instance_meta_fetching
state :instance_meta_error
state :instance_meta_done
state :configuring
state :projects_meta_fetching
state :projects_meta_error
state :projects_meta_done
state :importing
state :import_error
state :imported
state :completed
state :reverting
state :revert_error
state :reverted
STATES_ORDER = [
INITIAL,
INSTANCE_META_FETCHING,
INSTANCE_META_ERROR,
INSTANCE_META_DONE,
CONFIGURING,
PROJECTS_META_FETCHING,
PROJECTS_META_ERROR,
PROJECTS_META_DONE,
IMPORTING,
IMPORT_ERROR,
IMPORTED,
COMPLETED,
REVERTING,
REVERT_ERROR,
REVERTED
].freeze
transition from: INITIAL, to: [INSTANCE_META_FETCHING]
transition from: INSTANCE_META_FETCHING, to: [INSTANCE_META_DONE, INSTANCE_META_ERROR]
transition from: INSTANCE_META_ERROR, to: [INSTANCE_META_FETCHING]
transition from: INSTANCE_META_DONE, to: [CONFIGURING]
transition from: CONFIGURING, to: [PROJECTS_META_FETCHING]
transition from: PROJECTS_META_FETCHING, to: [PROJECTS_META_DONE, PROJECTS_META_ERROR]
transition from: PROJECTS_META_ERROR, to: [PROJECTS_META_FETCHING]
transition from: PROJECTS_META_DONE, to: [IMPORTING]
transition from: IMPORTING, to: [IMPORTED, IMPORT_ERROR]
transition from: IMPORT_ERROR, to: [IMPORTING]
transition from: IMPORTED, to: [COMPLETED, REVERTING]
transition from: REVERTING, to: [REVERTED, REVERT_ERROR]
after_transition(to: :instance_meta_fetching) do |jira_import, transition|
JiraInstanceMetaDataJob.perform_later(jira_import.id)
end
after_transition(to: :projects_meta_fetching) do |jira_import, transition|
JiraProjectsMetaDataJob.perform_later(jira_import.id)
end
after_transition(to: :importing) do |jira_import, transition|
JiraFetchAndImportProjectsJob.perform_later(jira_import.id)
end
after_transition(to: :reverting) do |jira_import, transition|
JiraRevertJiraImportJob.perform_later(jira_import.id)
end
def status_running?
[
INSTANCE_META_FETCHING,
PROJECTS_META_FETCHING,
IMPORTING,
REVERTING
].include?(current_state)
end
def status_equal_or_after?(check_status)
STATES_ORDER.index(current_state) >= STATES_ORDER.index(check_status)
end
def status_equal_or_before?(check_status)
STATES_ORDER.index(current_state) <= STATES_ORDER.index(check_status)
end
def status_before?(check_status)
STATES_ORDER.index(current_state) < STATES_ORDER.index(check_status)
end
def status_after?(check_status)
STATES_ORDER.index(current_state) > STATES_ORDER.index(check_status)
end
def deletable?
!status_running? && !in_state?(IMPORTED, IMPORT_ERROR, REVERT_ERROR)
end
end
+13
View File
@@ -0,0 +1,13 @@
class JiraImportTransition < ApplicationRecord
belongs_to :jira_import, inverse_of: :transitions
after_destroy :update_most_recent, if: :most_recent?
private
def update_most_recent
last_transition = jira_import.jira_import_transitions.order(:sort_key).last
return unless last_transition.present?
last_transition.update_column(:most_recent, true)
end
end
@@ -0,0 +1,101 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<% html_title t(:label_administration), JiraImport.model_name, @jira_import.id %>
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title do
concat(render(Primer::Beta::Text.new(mr: 2)) {
"#{I18n.t(:"admin.jira.run.title")} #{@jira_import.id.to_s}"
})
concat(render(Admin::Import::Jira::ImportRuns::StreamableStatusBadgeComponent.new(@jira_import.current_state)))
end
header.with_breadcrumbs(
[
{ href: admin_index_path, text: t(:label_administration) },
{ href: admin_import_path, text: t(:"admin.import.title") },
{ href: admin_import_jira_index_path, text: I18n.t(:"admin.jira.title") },
{ href: admin_import_jira_path(@jira), text: @jira.name || @jira.url },
{ href: admin_import_jira_run_path(id: @jira_import.id), text: "Run: #{@jira_import.id}" },
I18n.t(:"admin.jira.run.history"),
]
)
end
%>
<%=
table = Class.new(OpPrimer::BorderBoxTableComponent) do
columns :from_state, :to_state, :metadata
def self.name
"HistoryTable"
end
def mobile_title
"History Table"
end
def row_class
@row_class ||= Class.new(OpPrimer::BorderBoxRowComponent) do
def self.name
"HistoryRow"
end
def button_links
[]
end
def from_state
model.from_state
end
def to_state
model.to_state
end
def metadata
model.metadata
end
end
end
def has_actions?
false
end
def headers
[
[:from_state, { caption: "From state" }],
[:to_state, { caption: "To state" }],
[:metadata, { caption: "Metadata" }]
]
end
end
render(table.new(rows: @history))
%>
@@ -34,7 +34,7 @@ See COPYRIGHT and LICENSE files for more details.
concat(render(Primer::Beta::Text.new(mr: 2)) {
"#{I18n.t(:"admin.jira.run.title")} #{@jira_import.id.to_s}"
})
concat(render(Admin::Import::Jira::ImportRuns::StreamableStatusBadgeComponent.new(@jira_import.status)))
concat(render(Admin::Import::Jira::ImportRuns::StreamableStatusBadgeComponent.new(@jira_import.current_state)))
end
header.with_breadcrumbs(
[
@@ -6,8 +6,9 @@ class JiraFetchAndImportProjectsJob < ApplicationJob
JiraFetchProjectsJob.perform_now(jira_import_id)
JiraImportProjectsJob.perform_now(jira_import_id)
jira_import.update!(status: JiraImport::IMPORTED, job_id: nil)
jira_import.transition_to!(:imported)
rescue StandardError => e
jira_import.update!(status: JiraImport::IMPORT_ERROR, job_id: nil, error: e.message)
jira_import.transition_to!(:import_error, error: e.message)
jira_import.update!(job_id: nil, error: e.message)
end
end
+4 -2
View File
@@ -47,9 +47,11 @@ class JiraInstanceMetaDataJob < ApplicationJob
jira = jira_import.jira
client = JiraClient.new(url: jira.url, personal_access_token: jira.personal_access_token)
available = collect_metadata(client)
jira_import.update!(status: JiraImport::INSTANCE_META_DONE, job_id: nil, available:, error: nil)
jira_import.update!(job_id: nil, available:, error: nil)
jira_import.transition_to!(:instance_meta_done)
rescue StandardError => e
jira_import.update!(status: JiraImport::INSTANCE_META_ERROR, job_id: nil, error: e.message)
jira_import.transition_to!(:instance_meta_error, error: e.message)
jira_import.update!(job_id: nil, error: e.message)
end
def collect_metadata(client)
+5 -3
View File
@@ -41,15 +41,17 @@ class JiraProjectsMetaDataJob < ApplicationJob
def perform(jira_import_id)
jira_import = JiraImport.find(jira_import_id)
get_meta(jira_import)
rescue StandardError => e
jira_import.transition_to!(:projects_meta_error, error: e.message)
jira_import.update!(job_id: nil, error: e.message)
end
def get_meta(jira_import)
jira = jira_import.jira
client = JiraClient.new(url: jira.url, personal_access_token: jira.personal_access_token)
selected = collect_metadata(client, jira_import.project_ids)
jira_import.update!(status: JiraImport::PROJECTS_META_DONE, job_id: nil, selected:, error: nil)
rescue StandardError => e
jira_import.update!(status: JiraImport::PROJECTS_META_ERROR, job_id: nil, error: e.message)
jira_import.transition_to!(:projects_meta_done, selected:)
jira_import.update!(job_id: nil, selected:, error: nil)
end
def collect_metadata(client, project_ids)
+2 -2
View File
@@ -65,9 +65,9 @@ class JiraRevertJiraImportJob < ApplicationJob
OpenProjectJiraReference.where(jira_import_id: jira_import.id).delete_all
jira_import.update!(status: JiraImport::REVERTED, job_id: nil)
jira_import.transition_to!(:reverted)
end
rescue StandardError => e
jira_import.update!(status: JiraImport::REVERT_ERROR, job_id: nil, error: e.message)
jira_import.transition_to!(:revert_error, job_id: nil, error: e.message)
end
end
+4
View File
@@ -0,0 +1,4 @@
# config/initializers/statesman.rb
Statesman.configure do
storage_adapter(Statesman::Adapters::ActiveRecord)
end
+1
View File
@@ -164,6 +164,7 @@ en:
label_ago: "%{amount} ago"
run:
title: "Import run"
history: "History"
remove_error: "A Jira import cannot be removed while it is running"
blank:
title: "No import runs set up yet"
+1
View File
@@ -742,6 +742,7 @@ Rails.application.routes.draw do
delete :remove
get :revert_modal
get :history
end
resource :select_projects,
@@ -0,0 +1,32 @@
# frozen_string_literal: true
class CreateJiraImportTransitions < ActiveRecord::Migration[8.0]
def change
create_table :jira_import_transitions do |t|
t.string :from_state, null: false
t.string :to_state, null: false
t.jsonb :metadata, default: {}
t.integer :sort_key, null: false
t.integer :jira_import_id, null: false
t.boolean :most_recent, null: false
# If you decide not to include an updated timestamp column in your transition
# table, you'll need to configure the `updated_timestamp_column` setting in your
# migration class.
t.timestamps null: false
end
# Foreign keys are optional, but highly recommended
add_foreign_key :jira_import_transitions, :jira_imports
add_index(:jira_import_transitions,
%i(jira_import_id sort_key),
unique: true,
name: "index_jira_import_transitions_parent_sort")
add_index(:jira_import_transitions,
%i(jira_import_id most_recent),
unique: true,
where: "most_recent",
name: "index_jira_import_transitions_parent_most_recent")
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class RemoveJiraImportStatus < ActiveRecord::Migration[8.0]
def change
remove_column :jira_imports, :status, :string
end
end