├── gemfile.d └── foreman-tasks.rb ├── lib ├── foreman-tasks.rb ├── foreman_tasks │ ├── version.rb │ ├── tasks │ │ ├── dynflow.rake │ │ ├── test.rake │ │ ├── reschedule_long_running_tasks_checker.rake │ │ └── generate_task_actions.rake │ ├── authorizer_ext.rb │ ├── task_error.rb │ ├── test_extensions.rb │ ├── triggers.rb │ ├── dynflow.rb │ ├── test_helpers.rb │ ├── dynflow │ │ └── configuration.rb │ └── continuous_output.rb └── tasks │ └── gettext.rake ├── webpack ├── ForemanTasks │ ├── Routes │ │ ├── ShowTask │ │ │ ├── showTask.scss │ │ │ ├── index.js │ │ │ ├── ShowTask.js │ │ │ └── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ └── ShowTask.test.js.snap │ │ │ │ └── ShowTask.test.js │ │ ├── ForemanTasksRouter.js │ │ ├── __snapshots__ │ │ │ ├── ForemanTasksRouter.test.js.snap │ │ │ └── ForemanTasksRoutes.test.js.snap │ │ ├── ForemanTasksRoutes.test.js │ │ ├── ForemanTasksRoutes.js │ │ └── ForemanTasksRouter.test.js │ ├── index.js │ ├── ForemanTasksSelectors.js │ ├── Components │ │ ├── TasksTable │ │ │ ├── TasksIndexPage.js │ │ │ ├── formatters │ │ │ │ ├── durationCellFormmatter.js │ │ │ │ ├── __test__ │ │ │ │ │ ├── dateCellFormmatter.test.js │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── dateCellFormmatter.test.js.snap │ │ │ │ │ │ ├── actionNameCellFormatter.test.js.snap │ │ │ │ │ │ ├── selectionHeaderCellFormatter.test.js.snap │ │ │ │ │ │ ├── selectionCellFormatter.test.js.snap │ │ │ │ │ │ ├── actionCellFormatter.test.js.snap │ │ │ │ │ │ └── durationCellFormmatter.test.js.snap │ │ │ │ │ ├── actionNameCellFormatter.test.js │ │ │ │ │ ├── selectionCellFormatter.test.js │ │ │ │ │ ├── actionCellFormatter.test.js │ │ │ │ │ ├── selectionHeaderCellFormatter.test.js │ │ │ │ │ └── durationCellFormmatter.test.js │ │ │ │ ├── dateCellFormmatter.js │ │ │ │ ├── actionNameCellFormatter.js │ │ │ │ ├── selectionHeaderCellFormatter.js │ │ │ │ ├── index.js │ │ │ │ ├── actionCellFormatter.js │ │ │ │ └── selectionCellFormatter.js │ │ │ ├── TasksTablePage.scss │ │ │ ├── __tests__ │ │ │ │ ├── TasksTable.test.js │ │ │ │ ├── TasksIndexPage.test.js │ │ │ │ ├── SubTasksPage.test.js │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── TasksTableActions.test.js.snap │ │ │ │ │ ├── TasksIndexPage.test.js.snap │ │ │ │ │ ├── TasksTable.test.js.snap │ │ │ │ │ └── SubTasksPage.test.js.snap │ │ │ │ ├── TasksTable.fixtures.js │ │ │ │ └── TasksTablePage.test.js │ │ │ ├── Components │ │ │ │ ├── __test__ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ ├── TableSelectionCell.test.js.snap │ │ │ │ │ │ ├── TableSelectionHeaderCell.test.js.snap │ │ │ │ │ │ └── ActionSelectButton.test.js.snap │ │ │ │ │ ├── ActionSelectButton.test.js │ │ │ │ │ ├── TableSelectionCell.test.js │ │ │ │ │ ├── TableSelectionHeaderCell.test.js │ │ │ │ │ └── SelectAllAlert.test.js │ │ │ │ ├── TableSelectionCell.js │ │ │ │ ├── ConfirmModal │ │ │ │ │ ├── ConfirmModalSelectors.js │ │ │ │ │ └── __test__ │ │ │ │ │ │ └── __snapshots__ │ │ │ │ │ │ └── ConfirmModalSelectors.test.js.snap │ │ │ │ ├── TableSelectionHeaderCell.js │ │ │ │ └── ActionSelectButton.js │ │ │ ├── SubTasksPage.js │ │ │ ├── TasksTableConstants.js │ │ │ └── index.js │ │ ├── TaskDetails │ │ │ ├── TaskDetailsConstants.js │ │ │ ├── TasksDetailsHelper.js │ │ │ ├── __tests__ │ │ │ │ ├── TaskDetails.fixtures.js │ │ │ │ ├── TaskDetailsActions.test.js │ │ │ │ ├── TaskDetails.test.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── TaskDetailsActions.test.js.snap │ │ │ ├── Components │ │ │ │ ├── __tests__ │ │ │ │ │ ├── Locks.test.js │ │ │ │ │ ├── TaskHelper.test.js │ │ │ │ │ ├── Task.test.js │ │ │ │ │ ├── Raw.test.js │ │ │ │ │ ├── RunningSteps.test.js │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ └── RunningSteps.test.js.snap │ │ │ │ │ └── Errors.test.js │ │ │ │ ├── TaskHelper.js │ │ │ │ └── TaskSkeleton.js │ │ │ └── TaskDetails.scss │ │ ├── TasksDashboard │ │ │ ├── Components │ │ │ │ ├── TasksTimeRow │ │ │ │ │ ├── TasksTimeRow.scss │ │ │ │ │ ├── TasksTimeRow.test.js │ │ │ │ │ ├── Components │ │ │ │ │ │ └── TimeDropDown │ │ │ │ │ │ │ └── TimeDropDown.test.js │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ └── TasksTimeRow.test.js.snap │ │ │ │ │ └── TasksTimeRow.js │ │ │ │ ├── TasksCardsGrid │ │ │ │ │ ├── Components │ │ │ │ │ │ ├── PausedTasksCard │ │ │ │ │ │ │ ├── PausedTasksCard.test.js │ │ │ │ │ │ │ ├── PausedTasksCard.js │ │ │ │ │ │ │ └── __snapshots__ │ │ │ │ │ │ │ │ └── PausedTasksCard.test.js.snap │ │ │ │ │ │ ├── RunningTasksCard │ │ │ │ │ │ │ ├── RunningTasksCard.test.js │ │ │ │ │ │ │ ├── RunningTasksCard.js │ │ │ │ │ │ │ └── __snapshots__ │ │ │ │ │ │ │ │ └── RunningTasksCard.test.js.snap │ │ │ │ │ │ ├── StoppedTasksCard │ │ │ │ │ │ │ ├── OtherInfo.test.js │ │ │ │ │ │ │ ├── StoppedTasksCard.test.js │ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ │ └── OtherInfo.test.js.snap │ │ │ │ │ │ │ └── StoppedTasksCard.scss │ │ │ │ │ │ ├── TasksDonutCard │ │ │ │ │ │ │ ├── TasksDonutCard.scss │ │ │ │ │ │ │ └── TasksDonutCard.test.js │ │ │ │ │ │ ├── TasksDonutChart │ │ │ │ │ │ │ ├── TasksDonutChart.scss │ │ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ │ │ └── TasksDonutChartHelper.test.js.snap │ │ │ │ │ │ │ ├── TasksDonutChartConstants.js │ │ │ │ │ │ │ └── TasksDonutChart.test.js │ │ │ │ │ │ └── ScheduledTasksCard │ │ │ │ │ │ │ ├── ScheduledTasksCard.scss │ │ │ │ │ │ │ └── ScheduledTasksCard.test.js │ │ │ │ │ ├── TasksCardsGrid.fixtures.js │ │ │ │ │ └── TasksCardsGrid.test.js │ │ │ │ └── TasksLabelsRow │ │ │ │ │ ├── TasksLabelsRow.scss │ │ │ │ │ └── __snapshots__ │ │ │ │ │ └── TasksLabelsRow.test.js.snap │ │ │ ├── TasksDashboardPropTypes.js │ │ │ ├── __tests__ │ │ │ │ ├── TasksDashboard.test.js │ │ │ │ ├── TasksDashboardHelper.test.js │ │ │ │ ├── TasksDashboardActions.test.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── TasksDashboard.test.js.snap │ │ │ ├── index.js │ │ │ └── TasksDashboard.scss │ │ ├── common │ │ │ ├── urlHelpers.js │ │ │ └── ToastsHelpers │ │ │ │ ├── ToastTypesConstants.js │ │ │ │ └── index.js │ │ └── TaskActions │ │ │ └── TaskActionsConstants.js │ ├── __snapshots__ │ │ └── ForemanTasks.test.js.snap │ ├── ForemanTasks.js │ ├── ForemanTasks.test.js │ └── ForemanTasksReducers.js ├── __mocks__ │ └── foremanReact │ │ ├── common │ │ ├── urlHelpers.js │ │ ├── I18n.js │ │ └── helpers.js │ │ ├── components │ │ ├── Pagination │ │ │ └── index.js │ │ ├── Layout │ │ │ └── LayoutActions.js │ │ ├── common │ │ │ ├── dates │ │ │ │ ├── RelativeDateTime.js │ │ │ │ └── LongDateTime.js │ │ │ ├── ActionButtons │ │ │ │ └── ActionButtons.js │ │ │ ├── MessageBox.js │ │ │ ├── table.js │ │ │ └── table │ │ │ │ └── actionsHelpers │ │ │ │ └── actionTypeCreator.js │ │ └── ToastsList │ │ │ └── index.js │ │ ├── redux │ │ ├── middlewares │ │ │ └── IntervalMiddleware.js │ │ └── API │ │ │ ├── index.js │ │ │ └── APISelectors.js │ │ ├── routes │ │ └── common │ │ │ └── PageLayout │ │ │ ├── components │ │ │ └── ExportButton │ │ │ │ └── ExportButton.js │ │ │ └── PageLayout.js │ │ └── constants.js └── index.js ├── Gemfile ├── .prettierrc ├── .stylelintrc ├── babel.config.js ├── test ├── unit │ ├── config │ │ └── environment.rb │ ├── tasks_setting_test.rb │ └── summarizer_test.rb ├── support │ ├── dummy_task_group.rb │ ├── dummy_active_job.rb │ ├── dummy_recurring_dynflow_action.rb │ └── dummy_dynflow_action.rb ├── factories │ ├── recurring_logic_factory.rb │ └── triggering_factory.rb ├── graphql │ └── queries │ │ ├── tasks_query_test.rb │ │ ├── recurring_logics_query_test.rb │ │ ├── task_query_test.rb │ │ └── recurring_logic_test.rb ├── foreman_tasks_test_helper.rb ├── controllers │ └── recurring_logics_controller_test.rb ├── lib │ └── concerns │ │ └── polling_action_extensions_test.rb ├── tasks │ └── generate_task_actions_test.rb └── helpers │ └── foreman_tasks │ └── foreman_tasks_helper_test.rb ├── app ├── assets │ ├── javascripts │ │ └── foreman_tasks │ │ │ └── foreman_tasks.js │ └── stylesheets │ │ └── foreman_tasks │ │ ├── trigger_form.css │ │ └── foreman_tasks.css ├── views │ ├── foreman_tasks │ │ ├── api │ │ │ ├── tasks │ │ │ │ ├── index.json.rabl │ │ │ │ ├── show.json.rabl │ │ │ │ └── details.json.rabl │ │ │ ├── recurring_logics │ │ │ │ ├── show.json.rabl │ │ │ │ ├── update.json.rabl │ │ │ │ ├── index.json.rabl │ │ │ │ ├── main.json.rabl │ │ │ │ └── base.json.rabl │ │ │ └── locks │ │ │ │ └── show.json.rabl │ │ ├── recurring_logics │ │ │ ├── _tab_related.html.erb │ │ │ └── show.html.erb │ │ ├── task_groups │ │ │ ├── _detail.html.erb │ │ │ ├── _common.html.erb │ │ │ └── _tab_related.html.erb │ │ ├── tasks │ │ │ ├── _lock_card.html.erb │ │ │ ├── show.html.erb │ │ │ └── dashboard │ │ │ │ ├── _tasks_status.html.erb │ │ │ │ └── _latest_tasks_in_error_warning.html.erb │ │ └── layouts │ │ │ └── react.html.erb │ ├── tasks_mailer │ │ ├── long_tasks.text.erb │ │ └── long_tasks.html.erb │ └── common │ │ └── _trigger_form.html.erb ├── models │ └── foreman_tasks │ │ ├── task_group_member.rb │ │ ├── task │ │ ├── task_cancelled_exception.rb │ │ └── status_explicator.rb │ │ ├── tasks_mail_notification.rb │ │ ├── recurring_logic_cancelled_exception.rb │ │ ├── task_group.rb │ │ ├── concerns │ │ ├── host_action_subject.rb │ │ ├── user_extensions.rb │ │ └── action_subject.rb │ │ └── task_groups │ │ └── recurring_logic_task_group.rb ├── lib │ ├── actions │ │ ├── middleware │ │ │ ├── hide_secrets.rb │ │ │ ├── inherit_task_groups.rb │ │ │ ├── rails_executor_wrap.rb │ │ │ ├── load_setting_values.rb │ │ │ ├── recurring_logic.rb │ │ │ ├── keep_current_timezone.rb │ │ │ └── proxy_batch_triggering.rb │ │ ├── action_with_sub_plans.rb │ │ ├── serializers │ │ │ └── active_record_serializer.rb │ │ ├── foreman │ │ │ └── puppetclass │ │ │ │ └── import.rb │ │ ├── deliver_long_running_tasks_notification.rb │ │ ├── helpers │ │ │ ├── lifecycle_logging.rb │ │ │ ├── lock.rb │ │ │ └── with_continuous_output.rb │ │ └── recurring_action.rb │ └── foreman_tasks │ │ └── concerns │ │ └── polling_action_extensions.rb ├── helpers │ └── foreman_tasks │ │ └── tasks_helper.rb ├── controllers │ ├── foreman_tasks │ │ ├── react_controller.rb │ │ └── concerns │ │ │ ├── parameters │ │ │ ├── recurring_logic.rb │ │ │ └── triggering.rb │ │ │ └── hosts_controller_extension.rb │ └── concerns │ │ └── foreman_tasks │ │ └── find_tasks_common.rb ├── mailers │ └── tasks_mailer.rb ├── graphql │ ├── types │ │ ├── triggering.rb │ │ ├── recurring_logic.rb │ │ └── task.rb │ └── mutations │ │ └── recurring_logics │ │ └── cancel.rb └── services │ └── ui_notifications │ ├── tasks.rb │ └── tasks │ ├── task_paused_owner.rb │ ├── task_bulk_stop.rb │ ├── task_bulk_cancel.rb │ ├── tasks_running_long.rb │ └── task_bulk_resume.rb ├── locale ├── de │ └── LC_MESSAGES │ │ └── foreman_tasks.mo ├── en │ └── LC_MESSAGES │ │ └── foreman_tasks.mo ├── es │ └── LC_MESSAGES │ │ └── foreman_tasks.mo ├── fr │ └── LC_MESSAGES │ │ └── foreman_tasks.mo ├── ja │ └── LC_MESSAGES │ │ └── foreman_tasks.mo ├── ka │ └── LC_MESSAGES │ │ └── foreman_tasks.mo ├── ko │ └── LC_MESSAGES │ │ └── foreman_tasks.mo ├── ru │ └── LC_MESSAGES │ │ └── foreman_tasks.mo ├── pt_BR │ └── LC_MESSAGES │ │ └── foreman_tasks.mo ├── zh_CN │ └── LC_MESSAGES │ │ └── foreman_tasks.mo └── zh_TW │ └── LC_MESSAGES │ └── foreman_tasks.mo ├── .eslintrc ├── db ├── seeds.d │ ├── 60-dynflow_proxy_feature.rb │ ├── 61-foreman_tasks_bookmarks.rb │ ├── 20-foreman_tasks_permissions.rb │ └── 30-notification_blueprints.rb └── migrate │ ├── 20150814204140_add_task_type_value_index.rb │ ├── 20140324104010_remove_foreman_tasks_progress.rb │ ├── 20140813215942_add_parent_task_id.rb │ ├── 20181019135324_add_remote_task_operation.rb │ ├── 20190318153925_add_task_state_updated_at.foreman_tasks.rb │ ├── 20210708123832_add_parent_task_id_to_remote_tasks.foreman_tasks.rb │ ├── 20200519093217_drop_dynflow_allow_dangerous_actions_setting.foreman_tasks.rb │ ├── 20211123170430_tasks_settings_to_dsl_category.rb │ ├── 20171026082635_add_task_action.foreman_tasks.rb │ ├── 20151022123457_add_recurring_logic_state.rb │ ├── 20210720115251_add_purpose_to_recurring_logic.rb │ ├── 20150817102538_add_delay_attributes.rb │ ├── 20160920151810_add_more_lock_indexes.rb │ ├── 20150907124936_create_recurring_logic.rb │ ├── 20131209122644_create_foreman_tasks_locks.rb │ ├── 20151112152108_create_triggerings.rb │ ├── 20200611090846_add_task_lock_index_on_resource_type_and_task_id.rb │ ├── 20180207150921_add_remote_tasks.foreman_tasks.rb │ ├── 20160924213030_change_tasks_widget_names.rb │ ├── 20131205204140_create_foreman_tasks.rb │ ├── 20181206124952_migrate_non_exclusive_locks_to_links.foreman_tasks.rb │ ├── 20150907131503_create_task_groups.rb │ ├── 20181206131627_make_locks_exclusive.foreman_tasks.rb │ ├── 20181206123910_create_foreman_tasks_links.foreman_tasks.rb │ ├── 20181206131436_drop_old_locks.foreman_tasks.rb │ ├── 20180216092715_use_uuid.rb │ └── 20200517215015_rename_bookmarks_controller.rb ├── bin └── foreman-tasks ├── .tx └── config ├── .github └── workflows │ ├── js_tests.yml │ ├── release.yml │ ├── test-breaking.yaml │ └── ruby_tests.yml ├── script ├── rails └── npm_link_foreman_js.sh ├── .gitignore ├── deploy ├── foreman-tasks.service └── foreman-tasks.sysconfig └── package.json /gemfile.d/foreman-tasks.rb: -------------------------------------------------------------------------------- 1 | gem 'sqlite3' 2 | -------------------------------------------------------------------------------- /lib/foreman-tasks.rb: -------------------------------------------------------------------------------- 1 | require 'foreman_tasks' 2 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Routes/ShowTask/showTask.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './ForemanTasks'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Routes/ShowTask/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './ShowTask'; 2 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard", 4 | ], 5 | } 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@theforeman/builder/babel'], 3 | }; 4 | -------------------------------------------------------------------------------- /lib/foreman_tasks/version.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | VERSION = '11.0.6'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /test/unit/config/environment.rb: -------------------------------------------------------------------------------- 1 | # dummy appllication.rb - for unit testing dynflow-executor 2 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/common/urlHelpers.js: -------------------------------------------------------------------------------- 1 | export const getURIsearch = () => 'a=b'; 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/foreman_tasks/foreman_tasks.js: -------------------------------------------------------------------------------- 1 | /* 2 | *= require foreman_tasks/trigger_form.js 3 | */ 4 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/api/tasks/index.json.rabl: -------------------------------------------------------------------------------- 1 | collection @tasks 2 | extends "foreman_tasks/api/tasks/show" 3 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/ForemanTasksSelectors.js: -------------------------------------------------------------------------------- 1 | export const selectForemanTasks = state => state.foremanTasks || {}; 2 | -------------------------------------------------------------------------------- /test/support/dummy_task_group.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | class DummyTaskGroup < ForemanTasks::TaskGroup 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/assets/stylesheets/foreman_tasks/trigger_form.css: -------------------------------------------------------------------------------- 1 | div.form-group div.trigger_fields { 2 | margin-top: 15px; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/components/Pagination/index.js: -------------------------------------------------------------------------------- 1 | const Pagination = () => jest.fn(); 2 | export default Pagination; 3 | -------------------------------------------------------------------------------- /locale/de/LC_MESSAGES/foreman_tasks.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theforeman/foreman-tasks/HEAD/locale/de/LC_MESSAGES/foreman_tasks.mo -------------------------------------------------------------------------------- /locale/en/LC_MESSAGES/foreman_tasks.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theforeman/foreman-tasks/HEAD/locale/en/LC_MESSAGES/foreman_tasks.mo -------------------------------------------------------------------------------- /locale/es/LC_MESSAGES/foreman_tasks.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theforeman/foreman-tasks/HEAD/locale/es/LC_MESSAGES/foreman_tasks.mo -------------------------------------------------------------------------------- /locale/fr/LC_MESSAGES/foreman_tasks.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theforeman/foreman-tasks/HEAD/locale/fr/LC_MESSAGES/foreman_tasks.mo -------------------------------------------------------------------------------- /locale/ja/LC_MESSAGES/foreman_tasks.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theforeman/foreman-tasks/HEAD/locale/ja/LC_MESSAGES/foreman_tasks.mo -------------------------------------------------------------------------------- /locale/ka/LC_MESSAGES/foreman_tasks.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theforeman/foreman-tasks/HEAD/locale/ka/LC_MESSAGES/foreman_tasks.mo -------------------------------------------------------------------------------- /locale/ko/LC_MESSAGES/foreman_tasks.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theforeman/foreman-tasks/HEAD/locale/ko/LC_MESSAGES/foreman_tasks.mo -------------------------------------------------------------------------------- /locale/ru/LC_MESSAGES/foreman_tasks.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theforeman/foreman-tasks/HEAD/locale/ru/LC_MESSAGES/foreman_tasks.mo -------------------------------------------------------------------------------- /locale/pt_BR/LC_MESSAGES/foreman_tasks.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theforeman/foreman-tasks/HEAD/locale/pt_BR/LC_MESSAGES/foreman_tasks.mo -------------------------------------------------------------------------------- /locale/zh_CN/LC_MESSAGES/foreman_tasks.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theforeman/foreman-tasks/HEAD/locale/zh_CN/LC_MESSAGES/foreman_tasks.mo -------------------------------------------------------------------------------- /locale/zh_TW/LC_MESSAGES/foreman_tasks.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theforeman/foreman-tasks/HEAD/locale/zh_TW/LC_MESSAGES/foreman_tasks.mo -------------------------------------------------------------------------------- /app/views/foreman_tasks/api/recurring_logics/show.json.rabl: -------------------------------------------------------------------------------- 1 | object @recurring_logic 2 | 3 | extends 'foreman_tasks/api/recurring_logics/main' 4 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/api/recurring_logics/update.json.rabl: -------------------------------------------------------------------------------- 1 | object @recurring_logic 2 | 3 | extends "foreman_tasks/api/recurring_logics/show" 4 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/api/recurring_logics/index.json.rabl: -------------------------------------------------------------------------------- 1 | collection @recurring_logics 2 | 3 | extends 'foreman_tasks/api/recurring_logics/base' 4 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/components/Layout/LayoutActions.js: -------------------------------------------------------------------------------- 1 | export const showLoading = () => null; 2 | export const hideLoading = () => null; 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@theforeman/foreman"], 3 | "extends": ["plugin:@theforeman/foreman/core", "plugin:@theforeman/foreman/plugins"] 4 | } 5 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/recurring_logics/_tab_related.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= render 'foreman_tasks/task_groups/tab_related', :source => source %> 3 |
4 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/components/common/dates/RelativeDateTime.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default date =>

{`${date} time ago`}

; 4 | -------------------------------------------------------------------------------- /test/support/dummy_active_job.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | class DummyActiveJob < ApplicationJob 3 | def humanized_name 4 | "Dummy action" 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/seeds.d/60-dynflow_proxy_feature.rb: -------------------------------------------------------------------------------- 1 | f = Feature.where(:name => 'Dynflow').first_or_create 2 | raise "Unable to create proxy feature: #{format_errors f}" if f.nil? || f.errors.any? 3 | -------------------------------------------------------------------------------- /test/support/dummy_recurring_dynflow_action.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | class DummyRecurringDynflowAction < Actions::EntryAction 3 | include Actions::RecurringAction 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/foreman_tasks/task_group_member.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | class TaskGroupMember < ApplicationRecord 3 | belongs_to :task_group 4 | belongs_to :task 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/foreman_tasks/tasks/dynflow.rake: -------------------------------------------------------------------------------- 1 | namespace :dynflow do 2 | task :client do 3 | ::ForemanTasks.dynflow.config.remote = true 4 | ::ForemanTasks.dynflow.initialize! 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ActionButtons = props => ; 4 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/components/common/MessageBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const MessageBox = () =>
; 4 | export default MessageBox; 5 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/components/common/dates/LongDateTime.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const LongDateTime = value =>

{value}

; 4 | 5 | export default LongDateTime; 6 | -------------------------------------------------------------------------------- /db/migrate/20150814204140_add_task_type_value_index.rb: -------------------------------------------------------------------------------- 1 | class AddTaskTypeValueIndex < ActiveRecord::Migration[4.2] 2 | def change 3 | add_index :foreman_tasks_tasks, [:type, :label] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/TasksIndexPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TasksTablePage from './'; 3 | 4 | export const TasksIndexPage = props => ; 5 | -------------------------------------------------------------------------------- /app/models/foreman_tasks/task/task_cancelled_exception.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | class Task::TaskCancelledException < ::Foreman::Exception 3 | def backtrace 4 | [] 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20140324104010_remove_foreman_tasks_progress.rb: -------------------------------------------------------------------------------- 1 | class RemoveForemanTasksProgress < ActiveRecord::Migration[4.2] 2 | def change 3 | remove_column :foreman_tasks_tasks, :progress 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20140813215942_add_parent_task_id.rb: -------------------------------------------------------------------------------- 1 | class AddParentTaskId < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :foreman_tasks_tasks, :parent_task_id, :string, index: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/common/I18n.js: -------------------------------------------------------------------------------- 1 | export { sprintf } from 'jed'; 2 | 3 | export const translate = s => s; 4 | 5 | export const ngettext = s => s; 6 | 7 | export const documentLocale = () => 'en'; 8 | -------------------------------------------------------------------------------- /db/migrate/20181019135324_add_remote_task_operation.rb: -------------------------------------------------------------------------------- 1 | class AddRemoteTaskOperation < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :foreman_tasks_remote_tasks, :operation, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/components/ToastsList/index.js: -------------------------------------------------------------------------------- 1 | export const addToast = toast => ({ 2 | type: 'TOASTS_ADD', 3 | payload: { 4 | message: toast, 5 | }, 6 | }); 7 | 8 | export default addToast; 9 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware.js: -------------------------------------------------------------------------------- 1 | export const stopInterval = () => ({ 2 | type: 'stop', 3 | }); 4 | 5 | export const withInterval = (object, interval) => ({ ...object, interval }); 6 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/routes/common/PageLayout/components/ExportButton/ExportButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ExportButton = () => ; 4 | 5 | export default ExportButton; 6 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/api/recurring_logics/main.json.rabl: -------------------------------------------------------------------------------- 1 | object @recurring_logic 2 | 3 | extends 'foreman_tasks/api/recurring_logics/base' 4 | 5 | child :tasks => :tasks do 6 | extends 'foreman_tasks/api/tasks/show' 7 | end 8 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/__snapshots__/ForemanTasks.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ForemanTasks render without Props 1`] = ` 4 | 5 | 6 | 7 | `; 8 | -------------------------------------------------------------------------------- /app/models/foreman_tasks/tasks_mail_notification.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | class TasksMailNotification < MailNotification 3 | ALL = N_("Subscribe") 4 | 5 | def subscription_options 6 | [ALL] 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20190318153925_add_task_state_updated_at.foreman_tasks.rb: -------------------------------------------------------------------------------- 1 | class AddTaskStateUpdatedAt < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :foreman_tasks_tasks, :state_updated_at, :timestamp, index: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20210708123832_add_parent_task_id_to_remote_tasks.foreman_tasks.rb: -------------------------------------------------------------------------------- 1 | class AddParentTaskIdToRemoteTasks < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :foreman_tasks_remote_tasks, :parent_task_id, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Routes/ShowTask/ShowTask.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ShowTask = () => ( 4 |
5 |

Hello Foreman Tasks

6 |

show-task-page

7 |
8 | ); 9 | 10 | export default ShowTask; 11 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/common/helpers.js: -------------------------------------------------------------------------------- 1 | export const getURIQuery = jest.fn(() => ({})); 2 | 3 | export const isoCompatibleDate = date => date; 4 | export const noop = Function.prototype; 5 | 6 | export const foremanUrl = path => `foreman${path}`; 7 | -------------------------------------------------------------------------------- /bin/foreman-tasks: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | foreman_root = '/usr/share/foreman' 4 | Dir.chdir(foreman_root) 5 | require File.expand_path('./config/application', foreman_root) 6 | Dynflow::Rails::Daemon.new.run_background(ARGV.last, :foreman_root => foreman_root) 7 | -------------------------------------------------------------------------------- /db/migrate/20200519093217_drop_dynflow_allow_dangerous_actions_setting.foreman_tasks.rb: -------------------------------------------------------------------------------- 1 | class DropDynflowAllowDangerousActionsSetting < ActiveRecord::Migration[6.0] 2 | def up 3 | Setting.where(name: 'dynflow_allow_dangerous_actions').delete_all 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js: -------------------------------------------------------------------------------- 1 | export const FOREMAN_TASK_DETAILS = 'FOREMAN_TASK_DETAILS'; 2 | export const FOREMAN_TASK_DETAILS_SUCCESS = 'FOREMAN_TASK_DETAILS_SUCCESS'; 3 | 4 | export const TASK_STEP_CANCEL = 'TASK_STEP_CANCEL'; 5 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/durationCellFormmatter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const durationCellFormmatter = value => ( 4 | 5 | {value.text} 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/components/common/table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Table = () =>
; 4 | export const createTableReducer = jest.fn(controller => controller); 5 | export const cellFormatter = cell => cell; 6 | -------------------------------------------------------------------------------- /app/models/foreman_tasks/recurring_logic_cancelled_exception.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | class RecurringLogicCancelledException < ::Foreman::Exception 3 | def initialize(msg = N_("Cannot update a cancelled Recurring Logic.")) 4 | super 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20211123170430_tasks_settings_to_dsl_category.rb: -------------------------------------------------------------------------------- 1 | class TasksSettingsToDslCategory < ActiveRecord::Migration[6.0] 2 | def up 3 | Setting.where(category: 'Setting::ForemanTasks').update_all(category: 'Setting') if column_exists?(:settings, :category) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/TasksDetailsHelper.js: -------------------------------------------------------------------------------- 1 | // Get Task ID from URL. Split url by '/', filter non-empty values and get Task ID. 2 | export const getTaskID = () => 3 | window.location.pathname 4 | .split('/') 5 | .filter(i => i) 6 | .slice(-1)[0]; 7 | -------------------------------------------------------------------------------- /db/migrate/20171026082635_add_task_action.foreman_tasks.rb: -------------------------------------------------------------------------------- 1 | class AddTaskAction < ActiveRecord::Migration[4.2] 2 | def up 3 | add_column :foreman_tasks_tasks, :action, :string 4 | end 5 | 6 | def down 7 | remove_column :foreman_tasks_tasks, :action 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js: -------------------------------------------------------------------------------- 1 | export const minProps = { 2 | cancelStep: jest.fn(), 3 | taskReload: false, 4 | taskReloadStop: jest.fn(), 5 | taskReloadStart: jest.fn(), 6 | taskProgressToggle: jest.fn(), 7 | status: 'RESOLVED', 8 | }; 9 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [o:foreman:p:foreman:r:foreman_tasks] 5 | file_filter = locale//foreman_tasks.edit.po 6 | source_file = locale/foreman_tasks.pot 7 | source_lang = en 8 | type = PO 9 | minimum_perc = 0 10 | resource_name = foreman_tasks 11 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksTimeRow/TasksTimeRow.scss: -------------------------------------------------------------------------------- 1 | .tasks-time-row { 2 | margin: 0; 3 | padding: 0 10px; 4 | 5 | .time-label { 6 | font-size: 130%; 7 | font-weight: bold; 8 | margin-right: 10px; 9 | vertical-align: middle; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/__test__/dateCellFormmatter.test.js: -------------------------------------------------------------------------------- 1 | import { dateCellFormmatter } from '../dateCellFormmatter'; 2 | 3 | describe('dateCellFormmatter', () => { 4 | it('render', () => { 5 | expect(dateCellFormmatter('some-value')).toMatchSnapshot(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/dateCellFormmatter.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`dateCellFormmatter render 1`] = ` 4 | 9 | `; 10 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/common/urlHelpers.js: -------------------------------------------------------------------------------- 1 | import { foremanUrl } from 'foremanReact/common/helpers'; 2 | 3 | export const foremanTasksApiPath = path => 4 | foremanUrl(`/foreman_tasks/api/tasks/${path}`); 5 | 6 | export const foremanTasksPath = path => 7 | foremanUrl(`/foreman_tasks/tasks/${path}`); 8 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Routes/ShowTask/__tests__/__snapshots__/ShowTask.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ShowTask render without Props 1`] = ` 4 |
5 |

6 | Hello Foreman Tasks 7 |

8 |

9 | show-task-page 10 |

11 |
12 | `; 13 | -------------------------------------------------------------------------------- /.github/workflows/js_tests.yml: -------------------------------------------------------------------------------- 1 | name: JavaScript Testing 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test_js: 11 | name: JavaScript 12 | uses: theforeman/actions/.github/workflows/foreman_plugin_js.yml@v0 13 | with: 14 | plugin: foreman-tasks 15 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/common/ToastsHelpers/ToastTypesConstants.js: -------------------------------------------------------------------------------- 1 | export const INFO = 'info'; 2 | export const SUCCESS = 'success'; 3 | export const ERROR = 'error'; 4 | export const WARNING = 'warning'; 5 | 6 | export const TOAST_TYPES = { 7 | INFO, 8 | SUCCESS, 9 | ERROR, 10 | WARNING, 11 | }; 12 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/api/locks/show.json.rabl: -------------------------------------------------------------------------------- 1 | object @lock 2 | 3 | attributes :name, :resource_type, :resource_id 4 | node(:exclusive) { !locals[:link] } 5 | node(:link) do 6 | method = "#{@object.resource_type.underscore.split('/').first}_path".to_sym 7 | public_send(method, @object.resource_id) if defined?(method) 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20151022123457_add_recurring_logic_state.rb: -------------------------------------------------------------------------------- 1 | class AddRecurringLogicState < ActiveRecord::Migration[4.2] 2 | def up 3 | add_column :foreman_tasks_recurring_logics, :state, :string, :index => true 4 | end 5 | 6 | def down 7 | remove_column :foreman_tasks_recurring_logics, :state 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const PageLayout = ({ children }) =>
{children}
; 5 | 6 | PageLayout.propTypes = { 7 | children: PropTypes.node.isRequired, 8 | }; 9 | 10 | export default PageLayout; 11 | -------------------------------------------------------------------------------- /db/migrate/20210720115251_add_purpose_to_recurring_logic.rb: -------------------------------------------------------------------------------- 1 | class AddPurposeToRecurringLogic < ActiveRecord::Migration[6.0] 2 | def change 3 | add_column :foreman_tasks_recurring_logics, :purpose, :string 4 | add_index :foreman_tasks_recurring_logics, :purpose, unique: true, where: "state IN ('active', 'disabled')" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionNameCellFormatter.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`actionNameCellFormatter render 1`] = ` 4 | 8 | action-name 9 | 10 | `; 11 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/components/common/table/actionsHelpers/actionTypeCreator.js: -------------------------------------------------------------------------------- 1 | const createTableActionTypes = tableID => ({ 2 | REQUEST: `${tableID.toUpperCase()}_REQUEST`, 3 | SUCCESS: `${tableID.toUpperCase()}_SUCCESS`, 4 | FAILURE: `${tableID.toUpperCase()}_FAILURE`, 5 | }); 6 | 7 | export default createTableActionTypes; 8 | -------------------------------------------------------------------------------- /db/migrate/20150817102538_add_delay_attributes.rb: -------------------------------------------------------------------------------- 1 | class AddDelayAttributes < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :foreman_tasks_tasks, :start_at, :datetime, index: true, default: nil, null: true 4 | add_column :foreman_tasks_tasks, :start_before, :datetime, index: true, default: nil, null: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/redux/API/index.js: -------------------------------------------------------------------------------- 1 | export const API = { 2 | get: jest.fn(), 3 | put: jest.fn(), 4 | post: jest.fn(), 5 | delete: jest.fn(), 6 | patch: jest.fn(), 7 | }; 8 | 9 | export const get = data => ({ type: 'get-some-type', ...data }); 10 | export const post = data => ({ type: 'post-some-type', ...data }); 11 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/ForemanTasks.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import ForemanTasksRouter from './Routes/ForemanTasksRouter'; 4 | 5 | const ForemanTasks = () => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default ForemanTasks; 12 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/ForemanTasks.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import ForemanTasks from './ForemanTasks'; 4 | 5 | const fixtures = { 6 | 'render without Props': {}, 7 | }; 8 | 9 | describe('ForemanTasks', () => 10 | testComponentSnapshotsWithFixtures(ForemanTasks, fixtures)); 11 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('..', __dir__) 5 | ENGINE_PATH = File.expand_path('../lib/foreman_tasks/engine', __dir__) 6 | 7 | require 'rails/all' 8 | require 'rails/engine/commands' 9 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/dateCellFormmatter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LongDateTime from 'foremanReact/components/common/dates/LongDateTime'; 3 | import { translate as __ } from 'foremanReact/common/I18n'; 4 | 5 | export const dateCellFormmatter = value => ( 6 | 7 | ); 8 | -------------------------------------------------------------------------------- /app/lib/actions/middleware/hide_secrets.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | module Middleware 3 | class HideSecrets < ::Dynflow::Middleware 4 | def present 5 | action.input[:secrets] = 'Secrets hidden' if action.input.key?(:secrets) 6 | action.output[:secrets] = 'Secrets hidden' if action.output.key?(:secrets) 7 | pass 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/selectionHeaderCellFormatter.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`selectionHeaderCellFormatter render 1`] = ` 4 | 11 | `; 12 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/redux/API/APISelectors.js: -------------------------------------------------------------------------------- 1 | export const selectAPIResponse = (state, key) => ({ 2 | data: { 3 | text: 'some-data', 4 | key, 5 | }, 6 | }); 7 | 8 | export const selectAPIStatus = (state, key) => 'PENDING'; 9 | export const selectAPIByKey = (state, key) => state[key]; 10 | export const selectAPIError = (state, key) => ({ error: `${key} ERRROR` }); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/db/*.sqlite3-journal 6 | test/dummy/log/*.log 7 | test/dummy/tmp/ 8 | test/dummy/.sass-cache 9 | Gemfile.lock 10 | .idea 11 | .*.sw? 12 | locale/*.mo 13 | locale/*/*.pox 14 | locale/*/*.edit.po 15 | locale/*/*.po.time_stamp 16 | node_modules 17 | package-lock.json 18 | coverage/ 19 | locale/action_names.rb 20 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/task_groups/_detail.html.erb: -------------------------------------------------------------------------------- 1 | <%= N_('Task group common') %> 2 | <%= render 'foreman_tasks/task_groups/common', :task_group => task_group %> 3 | <%= task_group.humanized_name %> specific 4 |
5 | <%= begin; render task_group.to_partial_path.dup, :task_group => task_group; rescue ActionView::MissingTemplate => e; end %> 6 |
7 | 8 | -------------------------------------------------------------------------------- /app/helpers/foreman_tasks/tasks_helper.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | module TasksHelper 3 | def format_task_input(task) 4 | return '-' unless task 5 | task.action 6 | end 7 | 8 | def format_recurring_logic_limit(thing) 9 | if thing.nil? 10 | content_tag(:i, N_('Unlimited')) 11 | else 12 | thing 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/controllers/foreman_tasks/react_controller.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | class ReactController < ::ApplicationController 3 | def index 4 | render 'foreman_tasks/layouts/react', :layout => false 5 | end 6 | 7 | private 8 | 9 | def controller_permission 10 | :foreman_tasks 11 | end 12 | 13 | def action_permission 14 | :view 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/mailers/tasks_mailer.rb: -------------------------------------------------------------------------------- 1 | class TasksMailer < ApplicationMailer 2 | helper ApplicationHelper 3 | 4 | def long_tasks(report, opts = {}) 5 | return if report.tasks.empty? 6 | 7 | @report = report 8 | @subject = opts[:subject] 9 | @subject ||= _('Tasks pending since %s') % (@report.time - @report.interval) 10 | mail(to: report.user.mail, subject: @subject) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/__test__/actionNameCellFormatter.test.js: -------------------------------------------------------------------------------- 1 | import { actionNameCellFormatter } from '../actionNameCellFormatter'; 2 | 3 | describe('actionNameCellFormatter', () => { 4 | it('render', () => { 5 | const data = ['action-name', { rowData: { id: 'some-id' } }]; 6 | expect(actionNameCellFormatter('some-url')(...data)).toMatchSnapshot(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/actionNameCellFormatter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cellFormatter } from 'foremanReact/components/common/table'; 3 | 4 | export const actionNameCellFormatter = url => (value, { rowData: { id } }) => 5 | cellFormatter( 6 | 7 | {value} 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /db/migrate/20160920151810_add_more_lock_indexes.rb: -------------------------------------------------------------------------------- 1 | class AddMoreLockIndexes < ActiveRecord::Migration[4.2] 2 | def change 3 | add_index(:foreman_tasks_tasks, [:id, :state], 4 | :name => 'index_foreman_tasks_id_state') 5 | add_index(:foreman_tasks_locks, [:name, :resource_type, :resource_id], 6 | :name => 'index_foreman_tasks_locks_name_resource_type_resource_id') 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/TasksTablePage.scss: -------------------------------------------------------------------------------- 1 | .tasks-table { 2 | margin-bottom: 70px; 3 | 4 | .action-name-tasks-table { 5 | overflow-wrap: anywhere; 6 | } 7 | } 8 | 9 | .tasks-table-wrapper { 10 | .export-csv { 11 | float: right; 12 | } 13 | .search-bar { 14 | padding-bottom: 20px; 15 | } 16 | } 17 | 18 | .tasks-time-row .time-label { 19 | float: left; 20 | } 21 | -------------------------------------------------------------------------------- /app/models/foreman_tasks/task_group.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | class TaskGroup < ApplicationRecord 3 | has_many :task_group_members, :dependent => :destroy 4 | has_many :tasks, :through => :task_group_members, :dependent => :nullify 5 | 6 | def resource_name 7 | raise NotImplementedError 8 | end 9 | 10 | def resource 11 | raise NotImplementedError 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/selectionCellFormatter.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`selectionCellFormatter render 1`] = ` 4 | 12 | `; 13 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Routes/ShowTask/__tests__/ShowTask.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import ShowTask from '../ShowTask'; 4 | 5 | const fixtures = { 6 | 'render without Props': { 7 | history: { 8 | push: jest.fn(), 9 | }, 10 | }, 11 | }; 12 | 13 | describe('ShowTask', () => 14 | testComponentSnapshotsWithFixtures(ShowTask, fixtures)); 15 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/api/tasks/show.json.rabl: -------------------------------------------------------------------------------- 1 | object @task if @task 2 | 3 | extends 'api/v2/layouts/permissions' 4 | 5 | attributes :id, :label, :pending, :action 6 | attributes :username, :started_at, :ended_at, :duration, :state, :result, :progress 7 | attributes :input, :output, :humanized, :cli_example, :start_at 8 | node(:available_actions) { |t| { cancellable: t.execution_plan&.cancellable?, resumable: t.resumable? } } 9 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTable.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | import TasksTable from '../TasksTable'; 3 | import fixtures from './TasksTable.fixtures'; 4 | 5 | jest.mock('../TasksTableSchema'); 6 | describe('TasksTable', () => { 7 | describe('rendering', () => 8 | testComponentSnapshotsWithFixtures(TasksTable, fixtures)); 9 | }); 10 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Routes/ForemanTasksRouter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | 4 | import routes from './ForemanTasksRoutes'; 5 | 6 | const ForemanTasksRouter = () => ( 7 | 8 | {Object.entries(routes).map(([key, props]) => ( 9 | 10 | ))} 11 | 12 | ); 13 | 14 | export default ForemanTasksRouter; 15 | -------------------------------------------------------------------------------- /app/lib/foreman_tasks/concerns/polling_action_extensions.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | module Concerns 3 | module PollingActionExtensions 4 | def poll_intervals 5 | multiplier = Setting[:foreman_tasks_polling_multiplier] || 1 6 | 7 | # Prevent the intervals from going below 0.5 seconds 8 | super.map { |interval| [interval * multiplier, 0.5].max } 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/models/foreman_tasks/concerns/host_action_subject.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | module Concerns 3 | module HostActionSubject 4 | extend ActiveSupport::Concern 5 | include ForemanTasks::Concerns::ActionSubject 6 | 7 | def action_input_key 8 | 'host' 9 | end 10 | 11 | def available_locks 12 | [:read, :write, :import_facts] 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/foreman_tasks/task_groups/recurring_logic_task_group.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | module TaskGroups 3 | class RecurringLogicTaskGroup < ::ForemanTasks::TaskGroup 4 | has_one :recurring_logic, :foreign_key => :task_group_id, :dependent => :nullify 5 | 6 | alias resource recurring_logic 7 | 8 | def resource_name 9 | N_('Recurring logic') 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/actionCellFormatter.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`actionCellFormatter render 1`] = ` 4 | 15 | `; 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | # Pattern matched against refs/tags 6 | tags: 7 | - '**' 8 | 9 | jobs: 10 | release: 11 | name: Release gem 12 | runs-on: ubuntu-latest 13 | environment: release 14 | if: github.repository_owner == 'theforeman' 15 | 16 | permissions: 17 | id-token: write 18 | 19 | steps: 20 | - uses: voxpupuli/ruby-release@v0 21 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Routes/__snapshots__/ForemanTasksRouter.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ForemanTasksRouter render without Props 1`] = ` 4 | 5 | 10 | 15 | 16 | `; 17 | -------------------------------------------------------------------------------- /db/migrate/20150907124936_create_recurring_logic.rb: -------------------------------------------------------------------------------- 1 | class CreateRecurringLogic < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :foreman_tasks_recurring_logics do |t| 4 | t.string :cron_line, :null => false 5 | t.datetime :end_time 6 | t.integer :max_iteration 7 | t.integer :iteration, :default => 0 8 | t.integer :task_group_id, :index => true, :null => false 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/__test__/selectionCellFormatter.test.js: -------------------------------------------------------------------------------- 1 | import selectionCellFormatter from '../selectionCellFormatter'; 2 | 3 | describe('selectionCellFormatter', () => { 4 | it('render', () => { 5 | expect( 6 | selectionCellFormatter( 7 | { isSelected: () => true }, 8 | { rowIndex: 'some-index', rowData: {} } 9 | ) 10 | ).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/TableSelectionCell.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TableSelectionCell renders TableSelectionCell 1`] = ` 4 | 7 | 13 | 14 | `; 15 | -------------------------------------------------------------------------------- /lib/foreman_tasks/authorizer_ext.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | # Monkey path until http://projects.theforeman.org/issues/8919 is 3 | # resolved and released 4 | module AuthorizerExt 5 | extend ActiveSupport::Concern 6 | 7 | def resource_name(klass) 8 | if klass.respond_to?(:authorized_resource_name) 9 | klass.authorized_resource_name 10 | else 11 | super klass 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/api/recurring_logics/base.json.rabl: -------------------------------------------------------------------------------- 1 | object @recurring_logic 2 | 3 | attributes :id, :cron_line, :end_time, :iteration, :task_group_id, :state, 4 | :max_iteration, :purpose 5 | 6 | node(:task_count) { |rl| rl.tasks.count } 7 | node(:action) { |rl| rl.tasks.first.try(:action) } 8 | node(:last_occurence) { |rl| rl.last_task&.started_at } 9 | node(:next_occurence) { |rl| rl.next_task&.start_at if rl.state == 'active' } 10 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/__test__/actionCellFormatter.test.js: -------------------------------------------------------------------------------- 1 | import { actionCellFormatter } from '../actionCellFormatter'; 2 | 3 | describe('actionCellFormatter', () => { 4 | it('render', () => { 5 | const data = [ 6 | { cancellable: true }, 7 | { rowData: { action: 'some-name', id: 'some-id', canEdit: true } }, 8 | ]; 9 | expect(actionCellFormatter({})(...data)).toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/tasks/_lock_card.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | <%= lock.resource_type %> 5 |

6 |
7 | <%= format('id:%s', lock.resource_id) %>
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/__test__/selectionHeaderCellFormatter.test.js: -------------------------------------------------------------------------------- 1 | import selectionHeaderCellFormatter from '../selectionHeaderCellFormatter'; 2 | 3 | describe('selectionHeaderCellFormatter', () => { 4 | it('render', () => { 5 | expect( 6 | selectionHeaderCellFormatter( 7 | { allPageSelected: () => true, permissions: { edit: true } }, 8 | 'some-label' 9 | ) 10 | ).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/selectionHeaderCellFormatter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TableSelectionHeaderCell from '../Components/TableSelectionHeaderCell'; 3 | 4 | export default (selectionController, label) => ( 5 | 11 | ); 12 | -------------------------------------------------------------------------------- /.github/workflows/test-breaking.yaml: -------------------------------------------------------------------------------- 1 | name: Label a PR `breaks-robottelo` on appropriate comment 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | breaks-robottelo: 7 | uses: theforeman/actions/.github/workflows/breaks-robottelo.yml@v0 8 | permissions: 9 | pull-requests: write 10 | with: 11 | repo: ${{ github.repository }} 12 | issue: ${{ github.event.issue.number }} 13 | secrets: 14 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | 16 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/recurring_logics/show.html.erb: -------------------------------------------------------------------------------- 1 | <% title N_('Recurring logic') %> 2 | 3 | <% title_actions(button_group(recurring_logic_action_buttons(@recurring_logic))) %> 4 | 5 |
6 | <%= _('Details') %> 7 | <%= render @recurring_logic.task_group, :task_group => @recurring_logic.task_group %> 8 |
9 | 10 |
11 | <%= render 'tab_related', :source => @recurring_logic %> 12 |
13 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/PausedTasksCard/PausedTasksCard.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import PausedTasksCard from './PausedTasksCard'; 4 | 5 | const fixtures = { 6 | 'render with minimal props': {}, 7 | 'render with some props': { some: 'prop' }, 8 | }; 9 | 10 | describe('PausedTasksCard', () => 11 | testComponentSnapshotsWithFixtures(PausedTasksCard, fixtures)); 12 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/TasksDashboardPropTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { TASKS_DASHBOARD_AVAILABLE_TIMES } from './TasksDashboardConstants'; 3 | 4 | export const timePropType = PropTypes.oneOf( 5 | Object.values(TASKS_DASHBOARD_AVAILABLE_TIMES) 6 | ); 7 | 8 | export const queryPropType = PropTypes.shape({ 9 | state: PropTypes.string, 10 | result: PropTypes.string, 11 | mode: PropTypes.string, 12 | time: timePropType, 13 | }); 14 | -------------------------------------------------------------------------------- /deploy/foreman-tasks.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Foreman jobs daemon 3 | Documentation=https://github.com/theforeman/foreman-tasks 4 | After=network.target remote-fs.target nss-lookup.target 5 | 6 | [Service] 7 | Type=forking 8 | User=foreman 9 | TimeoutSec=600 10 | WorkingDirectory=/usr/share/foreman 11 | ExecStart=/usr/bin/foreman-tasks start 12 | ExecStop=/usr/bin/foreman-tasks stop 13 | EnvironmentFile=-/etc/sysconfig/foreman-tasks 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/RunningTasksCard/RunningTasksCard.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import RunningTasksCard from './RunningTasksCard'; 4 | 5 | const fixtures = { 6 | 'render with minimal props': {}, 7 | 'render with some props': { some: 'prop' }, 8 | }; 9 | 10 | describe('RunningTasksCard', () => 11 | testComponentSnapshotsWithFixtures(RunningTasksCard, fixtures)); 12 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboard.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import TasksDashboard from '../TasksDashboard'; 4 | 5 | const fixtures = { 6 | 'render without Props': { history: {} }, 7 | /** fixtures, props for the component */ 8 | }; 9 | 10 | describe('TasksDashboard', () => { 11 | describe('rendering', () => 12 | testComponentSnapshotsWithFixtures(TasksDashboard, fixtures)); 13 | }); 14 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/__tests__/TasksIndexPage.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | import { TasksIndexPage } from '../TasksIndexPage'; 3 | import { minProps } from './TasksTable.fixtures'; 4 | 5 | const fixtures = { 6 | 'render with minimal props': minProps, 7 | }; 8 | 9 | describe('TasksIndexPage', () => { 10 | describe('rendering', () => 11 | testComponentSnapshotsWithFixtures(TasksIndexPage, fixtures)); 12 | }); 13 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/__test__/__snapshots__/durationCellFormmatter.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`durationCellFormmatter render with tooltip 1`] = ` 4 | 8 | some-value 9 | 10 | `; 11 | 12 | exports[`durationCellFormmatter render without tooltip 1`] = ` 13 | 16 | some-value 17 | 18 | `; 19 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/task_groups/_common.html.erb: -------------------------------------------------------------------------------- 1 |
2 | '> 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
<%= _('ID') %><%= link_to(task_group.id, foreman_tasks_task_group_url(task_group)) %>
<%= _('Task count') %><%= link_to(task_group.tasks.count, foreman_tasks_tasks_url(:search => "task_group.id = #{task_group.id}")) %>
12 |
13 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/TableSelectionHeaderCell.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TableSelectionHeaderCell renders TableSelectionHeaderCell 1`] = ` 4 | 8 | 14 | 15 | `; 16 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/index.js: -------------------------------------------------------------------------------- 1 | export { default as selectionHeaderCellFormatter } from './selectionHeaderCellFormatter'; 2 | export { default as selectionCellFormatter } from './selectionCellFormatter'; 3 | 4 | export { actionNameCellFormatter } from './actionNameCellFormatter'; 5 | export { actionCellFormatter } from './actionCellFormatter'; 6 | export { dateCellFormmatter } from './dateCellFormmatter'; 7 | export { durationCellFormmatter } from './durationCellFormmatter'; 8 | -------------------------------------------------------------------------------- /lib/foreman_tasks/task_error.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | class TaskError < StandardError 3 | attr_reader :task 4 | attr_reader :errors 5 | 6 | def initialize(task) 7 | @task = task 8 | @errors = task.execution_plan.steps.values.map(&:error).compact 9 | super(aggregated_message) 10 | end 11 | 12 | def aggregated_message 13 | "Task #{task.id}: " + 14 | errors.map { |e| "#{e.exception_class}: #{e.message}" }.join('; ') 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/OtherInfo.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import { OtherInfo } from './OtherInfo'; 4 | 5 | const fixtures = { 6 | render: { 7 | updateQuery: jest.fn, 8 | otherCount: 7, 9 | query: { state: 'STOPPED', result: 'OTHER' }, 10 | }, 11 | }; 12 | 13 | describe('OtherInfo', () => 14 | testComponentSnapshotsWithFixtures(OtherInfo, fixtures)); 15 | -------------------------------------------------------------------------------- /db/migrate/20131209122644_create_foreman_tasks_locks.rb: -------------------------------------------------------------------------------- 1 | class CreateForemanTasksLocks < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :foreman_tasks_locks do |t| 4 | t.string :task_id, index: true, null: false 5 | t.string :name, index: true, null: false 6 | t.string :resource_type, index: true 7 | t.integer :resource_id 8 | t.boolean :exclusive, index: true 9 | end 10 | add_index :foreman_tasks_locks, [:resource_type, :resource_id] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/common/ToastsHelpers/index.js: -------------------------------------------------------------------------------- 1 | import { TOAST_TYPES } from './ToastTypesConstants'; 2 | 3 | export const successToastData = message => ({ 4 | type: TOAST_TYPES.SUCCESS, 5 | message, 6 | }); 7 | export const errorToastData = message => ({ type: TOAST_TYPES.ERROR, message }); 8 | export const warningToastData = message => ({ 9 | type: TOAST_TYPES.WARNING, 10 | message, 11 | }); 12 | export const infoToastData = message => ({ 13 | type: TOAST_TYPES.INFO, 14 | message, 15 | }); 16 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/TasksCardsGrid.fixtures.js: -------------------------------------------------------------------------------- 1 | export const MOCKED_DATA = { 2 | running: { 3 | last: 3, 4 | older: 5, 5 | }, 6 | paused: { 7 | last: 3, 8 | older: 5, 9 | }, 10 | stopped: { 11 | error: { 12 | total: 8, 13 | last: 1, 14 | }, 15 | warning: { 16 | total: 20, 17 | last: 2, 18 | }, 19 | success: { 20 | total: 25, 21 | last: 3, 22 | }, 23 | }, 24 | scheduled: 1, 25 | }; 26 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/Components/__test__/ActionSelectButton.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import { ActionSelectButton } from '../ActionSelectButton'; 4 | 5 | const fixtures = { 6 | 'renders with minimal props': { 7 | onCancel: jest.fn(), 8 | onResume: jest.fn(), 9 | onForceCancel: jest.fn(), 10 | }, 11 | }; 12 | 13 | describe('ActionSelectButton', () => 14 | testComponentSnapshotsWithFixtures(ActionSelectButton, fixtures)); 15 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/Components/__test__/TableSelectionCell.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import TableSelectionCell from '../TableSelectionCell'; 4 | 5 | const fixtures = { 6 | 'renders TableSelectionCell': { 7 | id: 'some id', 8 | label: 'some label', 9 | checked: true, 10 | onChange: jest.fn(), 11 | }, 12 | }; 13 | 14 | describe('TableSelectionCell', () => 15 | testComponentSnapshotsWithFixtures(TableSelectionCell, fixtures)); 16 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/__test__/durationCellFormmatter.test.js: -------------------------------------------------------------------------------- 1 | import { durationCellFormmatter } from '../durationCellFormmatter'; 2 | 3 | describe('durationCellFormmatter', () => { 4 | it('render with tooltip', () => { 5 | expect( 6 | durationCellFormmatter({ text: 'some-value', tooltip: 'some-tooltip' }) 7 | ).toMatchSnapshot(); 8 | }); 9 | it('render without tooltip', () => { 10 | expect(durationCellFormmatter({ text: 'some-value' })).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/TasksDonutCard/TasksDonutCard.scss: -------------------------------------------------------------------------------- 1 | .tasks-donut-card { 2 | &.not-focused { 3 | .card-pf-title, 4 | .card-pf-body { 5 | opacity: 0.6; 6 | } 7 | } 8 | .card-pf-title { 9 | text-align: center; 10 | font-size: 180%; 11 | cursor: pointer; 12 | } 13 | 14 | .card-pf-body { 15 | padding-left: 15px; 16 | } 17 | 18 | &.selected-tasks-card { 19 | .card-pf-title { 20 | font-weight: bold; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/lib/actions/action_with_sub_plans.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | class Actions::ActionWithSubPlans < Actions::EntryAction 3 | include Dynflow::Action::V2::WithSubPlans 4 | 5 | def plan(*_args) 6 | raise NotImplementedError 7 | end 8 | 9 | def humanized_output 10 | return unless counts_set? 11 | _('%{total} task(s), %{success} success, %{failed} fail') % 12 | { total: total_count, 13 | success: output[:success_count], 14 | failed: output[:failed_count] } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/TasksDonutChart/TasksDonutChart.scss: -------------------------------------------------------------------------------- 1 | .tasks-donut-chart { 2 | .c3-chart-arcs-title { 3 | cursor: pointer; 4 | 5 | &:hover { 6 | .donut-title-big-pf, 7 | .donut-title-small-pf { 8 | font-weight: bold; 9 | } 10 | } 11 | } 12 | 13 | &.tasks-donut-selected { 14 | .c3-chart-arcs-title { 15 | .donut-title-big-pf, 16 | .donut-title-small-pf { 17 | font-weight: bold; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /db/migrate/20151112152108_create_triggerings.rb: -------------------------------------------------------------------------------- 1 | class CreateTriggerings < ActiveRecord::Migration[4.2] 2 | def up 3 | create_table :foreman_tasks_triggerings do |t| 4 | t.string :mode, null: false 5 | t.datetime :start_at 6 | t.datetime :start_before 7 | end 8 | 9 | change_table :foreman_tasks_recurring_logics do |t| 10 | t.integer :triggering_id 11 | end 12 | end 13 | 14 | def down 15 | drop_table :foreman_tasks_triggerings 16 | remove_column :foreman_tasks_recurring_logics, :triggering_id 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsActions.test.js: -------------------------------------------------------------------------------- 1 | import { testActionSnapshotWithFixtures } from '@theforeman/test'; 2 | import { 3 | taskReloadStop, 4 | taskReloadStart, 5 | cancelStep, 6 | } from '../TaskDetailsActions'; 7 | 8 | const fixtures = { 9 | 'should start reload': () => taskReloadStart(1), 10 | 'should stop reload': () => taskReloadStop(), 11 | 'should cancelStep': () => cancelStep('task-id', 'step-id'), 12 | }; 13 | 14 | describe('TaskDetails - Actions', () => 15 | testActionSnapshotWithFixtures(fixtures)); 16 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/Components/__test__/TableSelectionHeaderCell.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import TableSelectionHeaderCell from '../TableSelectionHeaderCell'; 4 | 5 | const fixtures = { 6 | 'renders TableSelectionHeaderCell': { 7 | id: 'some id', 8 | label: 'some label', 9 | checked: true, 10 | onChange: jest.fn(), 11 | }, 12 | }; 13 | 14 | describe('TableSelectionHeaderCell', () => 15 | testComponentSnapshotsWithFixtures(TableSelectionHeaderCell, fixtures)); 16 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/ForemanTasksReducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { reducers as tasksDashboardReducers } from './Components/TasksDashboard'; 3 | import { reducers as tasksTableReducers } from './Components/TasksTable'; 4 | import { reducers as confirmModalReducers } from './Components/TasksTable/Components/ConfirmModal'; 5 | 6 | const reducers = { 7 | foremanTasks: combineReducers({ 8 | ...tasksDashboardReducers, 9 | ...tasksTableReducers, 10 | ...confirmModalReducers, 11 | }), 12 | }; 13 | 14 | export default reducers; 15 | -------------------------------------------------------------------------------- /.github/workflows/ruby_tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: pull_request 5 | 6 | concurrency: 7 | group: ${{ github.ref_name }}-${{ github.workflow }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | rubocop: 12 | name: Rubocop 13 | uses: theforeman/actions/.github/workflows/rubocop.yml@v0 14 | with: 15 | command: bundle exec rubocop --parallel --format github 16 | 17 | test: 18 | name: Ruby 19 | needs: rubocop 20 | uses: theforeman/actions/.github/workflows/foreman_plugin.yml@v0 21 | with: 22 | plugin: foreman-tasks 23 | -------------------------------------------------------------------------------- /app/controllers/concerns/foreman_tasks/find_tasks_common.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | module FindTasksCommon 3 | def search_query 4 | [current_taxonomy_search, params[:search]].select(&:present?).join(' AND ') 5 | end 6 | 7 | def current_taxonomy_search 8 | conditions = [] 9 | conditions << "organization_id = #{Organization.current.id}" if Organization.current 10 | conditions << "location_id = #{Location.current.id}" if Location.current 11 | conditions.empty? ? '' : "(#{conditions.join(' AND ')})" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/graphql/types/triggering.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class Triggering < Types::BaseObject 3 | description 'A Task Triggering' 4 | model_class ::ForemanTasks::Triggering 5 | 6 | global_id_field :id 7 | field :mode, String 8 | field :start_at, GraphQL::Types::ISO8601DateTime 9 | field :start_before, GraphQL::Types::ISO8601DateTime 10 | field :recurring_logic, Types::RecurringLogic 11 | 12 | def self.graphql_definition 13 | super.tap { |type| type.instance_variable_set(:@name, 'ForemanTasks::Triggering') } 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/TasksDonutChart/__snapshots__/TasksDonutChartHelper.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TasksDonutChartHelper should create chart-data 1`] = ` 4 | Object { 5 | "columns": Array [ 6 | Array [ 7 | "last", 8 | 1, 9 | ], 10 | Array [ 11 | "older", 12 | 3, 13 | ], 14 | ], 15 | "names": Object { 16 | "last": "1 Last 24h", 17 | "older": "3 Older 24h", 18 | }, 19 | "onItemClick": [Function], 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/layouts/react.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for(:javascripts) do %> 2 | <%= webpacked_plugins_js_for :'foreman-tasks' %> 3 | <% end %> 4 | <% content_for(:stylesheets) do %> 5 | <%= webpacked_plugins_css_for :'foreman-tasks' %> 6 | <% end %> 7 | 8 | <% content_for(:content) do %> 9 |
10 |
11 | <%= react_component('ForemanTasks') %> 12 | <% end %> 13 | <%= render template: "layouts/base" %> 14 | -------------------------------------------------------------------------------- /db/migrate/20200611090846_add_task_lock_index_on_resource_type_and_task_id.rb: -------------------------------------------------------------------------------- 1 | class AddTaskLockIndexOnResourceTypeAndTaskId < ActiveRecord::Migration[6.0] 2 | def change 3 | add_index :foreman_tasks_locks, [:task_id, :resource_type, :resource_id], name: 'index_tasks_locks_on_task_id_resource_type_and_resource_id' 4 | # These indexes are not needed as they can be gained from partial index lookups 5 | [:task_id, :name, :resource_type].each do |index| 6 | remove_index :foreman_tasks_locks, index if index_exists?(:foreman_tasks_locks, index) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/seeds.d/61-foreman_tasks_bookmarks.rb: -------------------------------------------------------------------------------- 1 | Bookmark.without_auditing do 2 | [ 3 | { :name => 'running', :query => 'state = running' }, 4 | { :name => 'failed', :query => 'state = paused or result = error or result = warning' }, 5 | 6 | ].each do |item| 7 | next if Bookmark.where(:name => item[:name]).first 8 | next if SeedHelper.audit_modified? Bookmark, item[:name] 9 | b = Bookmark.create({ :controller => 'foreman_tasks_tasks', :public => true }.merge(item)) 10 | raise "Unable to create bookmark: #{format_errors b}" if b.nil? || b.errors.any? 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/TasksDonutChart/TasksDonutChartConstants.js: -------------------------------------------------------------------------------- 1 | export const TASKS_DONUT_CHART_FOCUSED_ON_OPTIONS = { 2 | NORMAL: 'normal', // normal-mode 3 | TOTAL: 'total', // total-mode 4 | LAST: 'last', // last X mode 5 | OLDER: 'older', // older then X mode 6 | NONE: 'none', // unfocus-mode: another card is selected 7 | }; 8 | 9 | export const TASKS_DONUT_CHART_FOCUSED_ON_OPTIONS_ARRAY = Object.values( 10 | TASKS_DONUT_CHART_FOCUSED_ON_OPTIONS 11 | ); 12 | 13 | export const COLLOR_PATTERN = ['#C315C7', '#0089C9']; 14 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/TasksLabelsRow.scss: -------------------------------------------------------------------------------- 1 | @import 'foremanReact/common/variables'; 2 | 3 | .tasks-labels-row { 4 | margin: 0; 5 | padding: 0 10px; 6 | .title { 7 | font-weight: 600; 8 | font-size: 13px; 9 | } 10 | .label { 11 | font-size: 100%; 12 | 13 | margin-left: 5px; 14 | margin-right: 5px; 15 | a { 16 | padding-left: 10px; 17 | } 18 | } 19 | .pficon-close { 20 | color: $color-pf-white; 21 | } 22 | .compound-label-pf { 23 | margin-left: 0; 24 | margin: 10px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/factories/recurring_logic_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :recurring_logic, :class => ForemanTasks::RecurringLogic do 3 | cron_line { '* * * * *' } 4 | association :task_group 5 | end 6 | 7 | factory :task_group, :class => ::ForemanTasks::TaskGroup do 8 | type { "ForemanTasks::TaskGroups::RecurringLogicTaskGroup" } 9 | end 10 | factory :recurring_logic_task_group, :class => ::ForemanTasks::TaskGroups::RecurringLogicTaskGroup 11 | factory :task_group_member, :class => ::ForemanTasks::TaskGroupMember do 12 | association :task_group, :task 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksTimeRow/TasksTimeRow.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import { TASKS_DASHBOARD_AVAILABLE_TIMES } from '../../TasksDashboardConstants'; 4 | import TasksTimeRow from './TasksTimeRow'; 5 | 6 | const fixtures = { 7 | 'render with minimal props': {}, 8 | 'render with props': { 9 | time: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK, 10 | updateTime: jest.fn(), 11 | }, 12 | }; 13 | 14 | describe('TasksTimeRow', () => 15 | testComponentSnapshotsWithFixtures(TasksTimeRow, fixtures)); 16 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/__tests__/SubTasksPage.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | import { SubTasksPage } from '../SubTasksPage'; 3 | import { minProps } from './TasksTable.fixtures'; 4 | 5 | const fixtures = { 6 | 'render with minimal props': { 7 | ...minProps, 8 | 9 | match: { 10 | params: { 11 | id: 'some-id', 12 | }, 13 | }, 14 | }, 15 | }; 16 | 17 | describe('SubTasksPage', () => { 18 | describe('rendering', () => 19 | testComponentSnapshotsWithFixtures(SubTasksPage, fixtures)); 20 | }); 21 | -------------------------------------------------------------------------------- /webpack/__mocks__/foremanReact/constants.js: -------------------------------------------------------------------------------- 1 | export const STATUS = { 2 | PENDING: 'PENDING', 3 | RESOLVED: 'RESOLVED', 4 | ERROR: 'ERROR', 5 | }; 6 | 7 | export const getControllerSearchProps = ( 8 | controller, 9 | id = 'searchBar', 10 | canCreateBookmarks = true 11 | ) => ({ 12 | controller, 13 | autocomplete: { 14 | id, 15 | searchQuery: '', 16 | url: `${controller}/auto_complete_search`, 17 | useKeyShortcuts: true, 18 | }, 19 | bookmarks: { 20 | url: '/api/bookmarks', 21 | canCreateBookmarks, 22 | documentationUrl: `4.1.5Searching`, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /db/migrate/20180207150921_add_remote_tasks.foreman_tasks.rb: -------------------------------------------------------------------------------- 1 | class AddRemoteTasks < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :foreman_tasks_remote_tasks do |t| 4 | t.string :execution_plan_id, :null => false 5 | t.integer :step_id, :null => false 6 | t.string :state, :null => false, :default => 'new' 7 | 8 | t.string :proxy_url, :null => false 9 | t.string :remote_task_id 10 | 11 | t.index [:execution_plan_id, :step_id], :name => 'index_foreman_tasks_plan_id_and_step_id' 12 | 13 | t.datetime :created_at 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Routes/ForemanTasksRoutes.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from '@theforeman/test'; 3 | 4 | import ForemanTasksRoutes from './ForemanTasksRoutes'; 5 | 6 | describe('ForemanTasksRoutes', () => { 7 | it('should create routes', () => { 8 | Object.entries(ForemanTasksRoutes).forEach(([key, Route]) => { 9 | const RouteRender = Route.render; 10 | const component = shallow(); 11 | Route.renderResult = component; 12 | }); 13 | 14 | expect(ForemanTasksRoutes).toMatchSnapshot(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/tasks/show.html.erb: -------------------------------------------------------------------------------- 1 | <% stylesheet 'foreman_tasks/foreman_tasks' %> 2 | <% content_for(:javascripts) do %> 3 | <%= webpacked_plugins_js_for :'foreman-tasks' %> 4 | <% end %> 5 | <% content_for(:stylesheets) do %> 6 | <%= webpacked_plugins_css_for :'foreman-tasks' %> 7 | <% end %> 8 | 9 | <% title _("Details of %s task") % @task.to_s %> 10 | 11 | <%= breadcrumbs( 12 | items: breadcrumb_items, 13 | name_field: 'action', 14 | resource_url: foreman_tasks_api_tasks_path, 15 | switcher_item_url: foreman_tasks_task_path(:id => ':id') 16 | ) %> 17 | 18 | <%= react_component('TaskDetails') %> 19 | -------------------------------------------------------------------------------- /app/models/foreman_tasks/task/status_explicator.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | class Task::StatusExplicator 3 | ANY = 1 4 | ERRONEOUS_STATUSES = [ 5 | { :state => 'paused', :result => ANY }, 6 | { :state => ANY, :result => 'error' }, 7 | { :state => ANY, :result => 'warning' }, 8 | ].freeze 9 | def is_erroneous(task) 10 | remainder = ERRONEOUS_STATUSES.select do |status| 11 | (status[:state] == ANY || status[:state] == task.state) && 12 | (status[:result] == ANY || status[:result] == task.result) 13 | end 14 | !remainder.empty? 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/views/tasks_mailer/long_tasks.text.erb: -------------------------------------------------------------------------------- 1 | <%= _("Tasks lingering in states %{states} since %{time}") % { 2 | time: @report.time - @report.interval, 3 | states: @report.states.join(', ') 4 | } %> 5 | 6 | More details: <%= foreman_tasks_tasks_url(search: @report.query) %> 7 | 8 | <% @report.tasks.each do |task| %> 9 | <%= _('ID') %>: <%= task.id %> 10 | <%= _('Action') %>: <%= task.action %> 11 | <%= _('Label') %>: <%= task.label %> 12 | <%= _('State') %>: <%= task.state %> 13 | <%= _('State updated at') %>: <%= task.state_updated_at %> 14 | <%= _('Details') %>: <%= foreman_tasks_task_url(task) %> 15 | 16 | <% end %> 17 | -------------------------------------------------------------------------------- /db/migrate/20160924213030_change_tasks_widget_names.rb: -------------------------------------------------------------------------------- 1 | class ChangeTasksWidgetNames < ActiveRecord::Migration[4.2] 2 | def up 3 | Widget.where(:name => 'Tasks Status table')\ 4 | .update_all(:name => 'Task Status') 5 | Widget.where(:name => 'Tasks in Error/Warning')\ 6 | .update_all(:name => 'Latest Warning/Error Tasks') 7 | end 8 | 9 | def down 10 | Widget.where(:name => 'Task Status')\ 11 | .update_all(:name => 'Tasks Status table') 12 | Widget.where(:name => 'Latest Warning/Error Tasks')\ 13 | .update_all(:name => 'Tasks in Error/Warning') 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/actionCellFormatter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { cellFormatter } from 'foremanReact/components/common/table'; 3 | import { ActionButton } from '../../common/ActionButtons/ActionButton'; 4 | 5 | export const actionCellFormatter = taskActions => ( 6 | value, 7 | { rowData: { action, id, canEdit } } 8 | ) => 9 | cellFormatter( 10 | canEdit && ( 11 | 18 | ) 19 | ); 20 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/ScheduledTasksCard/ScheduledTasksCard.scss: -------------------------------------------------------------------------------- 1 | .scheduled-tasks-card { 2 | text-align: center; 3 | 4 | .scheduled-data { 5 | margin-top: 30px; 6 | padding-right: 15px; 7 | cursor: pointer; 8 | font-size: 40px; 9 | font-weight: 300; 10 | transition: font-weight 50ms ease-in; 11 | 12 | p { 13 | font-size: 20px; 14 | margin: 0; 15 | } 16 | 17 | * { 18 | margin: 10px; 19 | } 20 | 21 | &:hover { 22 | transition: font-weight 50ms ease-in; 23 | font-weight: 600; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/assets/stylesheets/foreman_tasks/foreman_tasks.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /db/seeds.d/20-foreman_tasks_permissions.rb: -------------------------------------------------------------------------------- 1 | view_permission = Permission.where(:name => :view_foreman_tasks, :resource_type => ForemanTasks::Task.name).first 2 | 3 | # the anonymous role was renamed to default in 4 | # https://github.com/theforeman/foreman/pull/3239 5 | default_role = Role.respond_to?(:default) ? Role.default : Role.anonymous 6 | # the view_permissions can be nil in tests: skipping in that case 7 | if view_permission && !default_role.permissions.include?(view_permission) 8 | default_role.filters.create(:search => 'owner.id = current_user') do |filter| 9 | filter.filterings.build { |f| f.permission = view_permission } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20131205204140_create_foreman_tasks.rb: -------------------------------------------------------------------------------- 1 | class CreateForemanTasks < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :foreman_tasks_tasks, :id => false, :primary_key => :id do |t| 4 | t.string :id, null: false 5 | t.string :type, index: true, null: false 6 | t.string :label, index: true 7 | t.datetime :started_at, index: true 8 | t.datetime :ended_at, index: true 9 | t.string :state, index: true, null: false 10 | t.string :result, index: true, null: false 11 | t.decimal :progress, index: true, precision: 5, scale: 4 12 | t.string :external_id, index: true 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCard.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | import { TASKS_DASHBOARD_AVAILABLE_QUERY_STATES } from '../../../../TasksDashboardConstants'; 3 | import StoppedTasksCard from './StoppedTasksCard'; 4 | 5 | const { STOPPED } = TASKS_DASHBOARD_AVAILABLE_QUERY_STATES; 6 | 7 | const fixtures = { 8 | 'render with minimal props': {}, 9 | 'render selected': { 10 | query: { state: STOPPED }, 11 | }, 12 | }; 13 | 14 | describe('StoppedTasksCard', () => 15 | testComponentSnapshotsWithFixtures(StoppedTasksCard, fixtures)); 16 | -------------------------------------------------------------------------------- /db/migrate/20181206124952_migrate_non_exclusive_locks_to_links.foreman_tasks.rb: -------------------------------------------------------------------------------- 1 | class MigrateNonExclusiveLocksToLinks < ActiveRecord::Migration[5.0] 2 | def up 3 | execute <<-SQL 4 | INSERT INTO foreman_tasks_links(task_id, resource_type, resource_id) 5 | SELECT DISTINCT locks.task_id, locks.resource_type, locks.resource_id 6 | FROM foreman_tasks_locks AS locks 7 | LEFT JOIN foreman_tasks_links AS links 8 | ON links.task_id = locks.task_id 9 | AND links.resource_type = locks.resource_type 10 | AND links.resource_id = locks.resource_id 11 | WHERE locks.exclusive = FALSE AND links.task_id IS NULL; 12 | SQL 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/lib/actions/serializers/active_record_serializer.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | module Serializers 3 | class ActiveRecordSerializer < ::Dynflow::Serializers::Noop 4 | def serialize(arg) 5 | if arg.is_a? ActiveRecord::Base 6 | { :active_record_object => true, 7 | :class_name => arg.class.name, 8 | :id => arg.id } 9 | else 10 | super arg 11 | end 12 | end 13 | 14 | def deserialize(arg) 15 | if arg.is_a?(Hash) && arg[:active_record_object] 16 | arg[:class_name].constantize.find(arg[:id]) 17 | else 18 | super arg 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /db/migrate/20150907131503_create_task_groups.rb: -------------------------------------------------------------------------------- 1 | class CreateTaskGroups < ActiveRecord::Migration[4.2] 2 | def up 3 | create_table :foreman_tasks_task_groups do |t| 4 | t.string :type, index: true, null: false 5 | end 6 | 7 | create_table :foreman_tasks_task_group_members do |t| 8 | t.string :task_id, null: false 9 | t.integer :task_group_id, null: false 10 | end 11 | 12 | add_index :foreman_tasks_task_group_members, [:task_id, :task_group_id], unique: true, name: 'foreman_tasks_task_group_members_index' 13 | end 14 | 15 | def down 16 | drop_table :foreman_tasks_task_groups 17 | drop_table :foreman_tasks_task_group_members 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/foreman_tasks/test_extensions.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | module TestExtensions 3 | module AccessPermissionsTestExtension 4 | def setup 5 | super 6 | if defined?(AccessPermissionsTest) && self.class == AccessPermissionsTest 7 | test_name = @method_name || @NAME 8 | # for compatibility with Foreman 1.10 9 | test_name = __name__ if test_name.nil? && respond_to?(:__name__) 10 | skip 'used by proxy only' if test_name.include?('foreman_tasks/api/tasks/callback') 11 | end 12 | end 13 | end 14 | end 15 | end 16 | 17 | ActiveSupport::TestCase.include ForemanTasks::TestExtensions::AccessPermissionsTestExtension 18 | -------------------------------------------------------------------------------- /app/services/ui_notifications/tasks.rb: -------------------------------------------------------------------------------- 1 | module UINotifications 2 | module Tasks 3 | class Base < ::UINotifications::Base 4 | def initialize(task) 5 | @subject = @task = task 6 | end 7 | 8 | def initiator 9 | User.anonymous_admin 10 | end 11 | 12 | def troubleshooting_help_generator 13 | return @troubleshooting_help_generator if defined? @troubleshooting_help_generator 14 | @troubleshooting_help_generator = if @task.main_action 15 | ForemanTasks::TroubleshootingHelpGenerator.new(@task.main_action) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/controllers/foreman_tasks/concerns/parameters/recurring_logic.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | module Concerns 3 | module Parameters 4 | module RecurringLogic 5 | extend ActiveSupport::Concern 6 | 7 | class_methods do 8 | def recurring_logic_params_filter 9 | Foreman::ParameterFilter.new(::ForemanTasks::RecurringLogic).tap do |filter| 10 | filter.permit(:enabled) 11 | end 12 | end 13 | end 14 | 15 | def recurring_logic_params 16 | self.class.recurring_logic_params_filter.filter_params(params, parameter_filter_context, :recurring_logic) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/graphql/types/recurring_logic.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class RecurringLogic < Types::BaseObject 3 | description 'A Recurring Logic' 4 | model_class ::ForemanTasks::RecurringLogic 5 | 6 | include ::Types::Concerns::MetaField 7 | 8 | global_id_field :id 9 | field :cron_line, String 10 | field :end_time, GraphQL::Types::ISO8601DateTime 11 | field :max_iteration, Integer 12 | field :iteration, Integer 13 | field :state, String 14 | field :purpose, String 15 | belongs_to :triggering, Types::Triggering 16 | 17 | def self.graphql_definition 18 | super.tap { |type| type.instance_variable_set(:@name, 'ForemanTasks::RecurringLogic') } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/formatters/selectionCellFormatter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { translate as __ } from 'foremanReact/common/I18n'; 3 | import TableSelectionCell from '../Components/TableSelectionCell'; 4 | 5 | export default (selectionController, additionalData) => ( 6 | selectionController.selectRow(additionalData)} 16 | /> 17 | ); 18 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/task_groups/_tab_related.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= _("Associated resources") %> 3 |
    4 | <% source.task_groups.pluck(:type).uniq.each do |type| %> 5 | <% next if type == source.task_group.type %> 6 | <% groups = source.task_groups.where(:type => type) %> 7 | <% begin %> 8 | <%= render groups.first.to_partial_path.pluralize.dup, :source => source, :task_groups => groups %> 9 | <% rescue ActionView::MissingTemplate => _ %> 10 | <% groups.each do |group| %> 11 | <%= render 'foreman_tasks/task_groups/common', :source => source, :task_group => group %> 12 | <% end %> 13 | <% end %> 14 | <% end %> 15 |
16 |
17 | 18 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/ScheduledTasksCard/ScheduledTasksCard.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import { TASKS_DASHBOARD_AVAILABLE_QUERY_STATES } from '../../../../TasksDashboardConstants'; 4 | import ScheduledTasksCard from './ScheduledTasksCard'; 5 | 6 | const fixtures = { 7 | 'render with minimal props': {}, 8 | 'render with props': { 9 | data: 3, 10 | className: 'some-class', 11 | }, 12 | 'render selected': { 13 | query: { state: TASKS_DASHBOARD_AVAILABLE_QUERY_STATES.SCHEDULED }, 14 | }, 15 | }; 16 | 17 | describe('ScheduledTasksCard', () => 18 | testComponentSnapshotsWithFixtures(ScheduledTasksCard, fixtures)); 19 | -------------------------------------------------------------------------------- /test/factories/triggering_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :triggering, :class => ForemanTasks::Triggering do 3 | mode { :immediate } 4 | 5 | trait :future do 6 | time = Time.zone.now + 1.year 7 | mode { :future } 8 | start_at { time } 9 | start_at_raw { time.strftime(ForemanTasks::Triggering::TIME_FORMAT) } 10 | end 11 | 12 | trait :recurring do 13 | mode { :recurring } 14 | input_type { :cronline } 15 | cronline { '* * * * *' } 16 | after(:build) { |triggering| triggering.recurring_logic = build(:recurring_logic) } 17 | end 18 | 19 | trait :end_time_limited do 20 | end_time_limited { true } 21 | end_time { Time.zone.now + 60 } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/graphql/queries/tasks_query_test.rb: -------------------------------------------------------------------------------- 1 | require 'foreman_tasks_test_helper' 2 | 3 | module Queries 4 | class TasksQueryTest < GraphQLQueryTestCase 5 | let(:query) do 6 | <<-GRAPHQL 7 | query { 8 | tasks { 9 | nodes { 10 | id 11 | action 12 | result 13 | } 14 | } 15 | } 16 | GRAPHQL 17 | end 18 | 19 | let(:data) { result['data']['tasks'] } 20 | 21 | setup do 22 | FactoryBot.create_list(:some_task, 2) 23 | end 24 | 25 | test "should fetch recurring logics" do 26 | assert_empty result['errors'] 27 | expected_count = ::ForemanTasks::Task.count 28 | assert_not_equal 0, expected_count 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Locks.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import Locks from '../Locks'; 4 | 5 | const fixtures = { 6 | 'render without Props': {}, 7 | 'render with Props': { 8 | locks: [ 9 | { 10 | name: 'task_owner', 11 | exclusive: false, 12 | resource_type: 'User', 13 | resource_id: 4, 14 | }, 15 | { 16 | name: 'task_owner2', 17 | exclusive: false, 18 | resource_type: 'User', 19 | resource_id: 2, 20 | }, 21 | ], 22 | }, 23 | }; 24 | 25 | describe('Locks', () => { 26 | describe('rendering', () => 27 | testComponentSnapshotsWithFixtures(Locks, fixtures)); 28 | }); 29 | -------------------------------------------------------------------------------- /app/lib/actions/foreman/puppetclass/import.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | module Foreman 3 | module Puppetclass 4 | class Import < Actions::EntryAction 5 | def resource_locks 6 | :import_puppetclasses 7 | end 8 | 9 | def run 10 | output[:results] = ::PuppetClassImporter.new.obsolete_and_new(input[:changed]) 11 | end 12 | 13 | def rescue_strategy 14 | ::Dynflow::Action::Rescue::Skip 15 | end 16 | 17 | def humanized_name 18 | _('Import Puppet classes') 19 | end 20 | 21 | # default value for cleaning up the tasks, it can be overriden by settings 22 | def self.cleanup_after 23 | '30d' 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/__snapshots__/OtherInfo.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`OtherInfo render 1`] = ` 4 | 7 | 12 | 13 | 16 | 17 | Other: 18 | 19 | 20 | 21 | 22 | 29 | 30 | `; 31 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Routes/ForemanTasksRoutes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TasksIndexPage } from '../Components/TasksTable/TasksIndexPage'; 3 | import { SubTasksPage } from '../Components/TasksTable/SubTasksPage'; 4 | import ShowTask from './ShowTask'; 5 | 6 | const ForemanTasksRoutes = { 7 | indexTasks: { 8 | path: '/foreman_tasks/tasks', 9 | exact: true, 10 | render: props => , 11 | }, 12 | subTasks: { 13 | path: '/foreman_tasks/tasks/:id/sub_tasks', 14 | exact: true, 15 | render: props => , 16 | }, 17 | showTask: { 18 | path: '/foreman_tasks/ex_tasks/:id', 19 | render: props => , 20 | }, 21 | }; 22 | 23 | export default ForemanTasksRoutes; 24 | -------------------------------------------------------------------------------- /test/graphql/queries/recurring_logics_query_test.rb: -------------------------------------------------------------------------------- 1 | require 'foreman_tasks_test_helper' 2 | 3 | module Queries 4 | class RecurringLogicsTest < GraphQLQueryTestCase 5 | let(:query) do 6 | <<-GRAPHQL 7 | query { 8 | recurringLogics { 9 | nodes { 10 | id 11 | cronLine 12 | } 13 | } 14 | } 15 | GRAPHQL 16 | end 17 | 18 | let(:data) { result['data']['recurringLogics'] } 19 | 20 | setup do 21 | FactoryBot.create_list(:recurring_logic, 2) 22 | end 23 | 24 | test "should fetch recurring logics" do 25 | assert_empty result['errors'] 26 | expected_count = ::ForemanTasks::RecurringLogic.count 27 | assert_not_equal 0, expected_count 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Routes/ForemanTasksRouter.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 3 | 4 | import ForemanTasksRouter from './ForemanTasksRouter'; 5 | 6 | jest.mock('./ForemanTasksRoutes', () => ({ 7 | someRoute: { 8 | path: '/some-route', 9 | render: props => some-route, 10 | }, 11 | someOtherRoute: { 12 | path: '/some-other-route', 13 | render: props => some-other-route, 14 | }, 15 | })); 16 | 17 | const fixtures = { 18 | 'render without Props': { 19 | history: { 20 | push: jest.fn(), 21 | }, 22 | }, 23 | }; 24 | 25 | describe('ForemanTasksRouter', () => 26 | testComponentSnapshotsWithFixtures(ForemanTasksRouter, fixtures)); 27 | -------------------------------------------------------------------------------- /lib/foreman_tasks/tasks/test.rake: -------------------------------------------------------------------------------- 1 | namespace :test do 2 | task :foreman_tasks => 'db:test:prepare' do 3 | test_task = Rake::TestTask.new('foreman_tasks_test_task') do |t| 4 | t.libs << ['test', "#{ForemanTasks::Engine.root}/test"] 5 | t.test_files = Dir["#{ForemanTasks::Engine.root}/test/**/*_test.rb"] 6 | t.verbose = true 7 | t.warning = false 8 | end 9 | 10 | Rake::Task[test_task.name].invoke 11 | end 12 | 13 | desc 'Alias for test:foreman_tasks' 14 | task :'foreman-tasks' => :foreman_tasks 15 | end 16 | 17 | Rake::Task[:test].enhance do 18 | Rake::Task['test:foreman_tasks'].invoke 19 | end 20 | 21 | load 'tasks/jenkins.rake' 22 | if Rake::Task.task_defined?(:'jenkins:unit') 23 | Rake::Task['jenkins:unit'].enhance ['test:foreman_tasks'] 24 | end 25 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksTimeRow/Components/TimeDropDown/TimeDropDown.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import { TASKS_DASHBOARD_AVAILABLE_TIMES } from '../../../../TasksDashboardConstants'; 4 | import TimeDropDown from './TimeDropDown'; 5 | 6 | const createRequiredProps = () => ({ id: 'some-id' }); 7 | 8 | const fixtures = { 9 | 'render with minimal props': { ...createRequiredProps() }, 10 | 'render with all props': { 11 | ...createRequiredProps(), 12 | className: 'some-class', 13 | selectedTime: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK, 14 | onChange: jest.fn(), 15 | }, 16 | }; 17 | 18 | describe('TimeDropDown', () => 19 | testComponentSnapshotsWithFixtures(TimeDropDown, fixtures)); 20 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js: -------------------------------------------------------------------------------- 1 | import { durationInWords } from '../TaskHelper'; 2 | 3 | describe('durationInWords', () => { 4 | it('should work for seconds', () => { 5 | expect(durationInWords('1/1/1 10:00:00', '1/1/1 10:00:01')).toEqual({ 6 | text: '1 second', 7 | tooltip: '1 seconds', 8 | }); 9 | }); 10 | it('should work for minutes', () => { 11 | expect(durationInWords('1/1/1 10:00:00', '1/1/1 10:02:01')).toEqual({ 12 | text: '2 minutes', 13 | tooltip: '121 seconds', 14 | }); 15 | }); 16 | it('should work for hours', () => { 17 | expect(durationInWords('1/1/1 10:00:00', '1/1/1 13:00:01')).toEqual({ 18 | text: '3 hours', 19 | tooltip: '10,801 seconds', 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /app/controllers/foreman_tasks/concerns/hosts_controller_extension.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | module Concerns 3 | module HostsControllerExtension 4 | def facts 5 | task = ForemanTasks.async_task(::Actions::Foreman::Host::ImportFacts, 6 | detect_host_type, 7 | params[:name], 8 | params[:facts], 9 | params[:certname], 10 | detected_proxy.try(:id)) 11 | 12 | render :json => { :task_id => task.id }, :status => :accepted 13 | rescue ::Foreman::Exception => e 14 | render :json => { 'message' => e.to_s }, :status => :unprocessable_entity 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/index.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import TasksDashboard from './TasksDashboard'; 4 | import * as actions from './TasksDashboardActions'; 5 | import reducer from './TasksDashboardReducer'; 6 | import { 7 | selectTime, 8 | selectQuery, 9 | selectTasksSummary, 10 | } from './TasksDashboardSelectors'; 11 | 12 | const mapStateToProps = state => ({ 13 | time: selectTime(state), 14 | query: selectQuery(state), 15 | tasksSummary: selectTasksSummary(state), 16 | }); 17 | 18 | const mapDispatchToProps = dispatch => bindActionCreators(actions, dispatch); 19 | 20 | export const reducers = { tasksDashboard: reducer }; 21 | 22 | export default connect(mapStateToProps, mapDispatchToProps)(TasksDashboard); 23 | -------------------------------------------------------------------------------- /app/lib/actions/deliver_long_running_tasks_notification.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | class DeliverLongRunningTasksNotification < EntryAction 3 | def plan(report) 4 | return if report.task_uuids.empty? 5 | 6 | plan_self report: report 7 | end 8 | 9 | def run 10 | report = OpenStruct.new(input[:report]) 11 | tasks = ForemanTasks::Task.where(id: report.task_uuids) 12 | report.user = User.current 13 | report.tasks = tasks 14 | ::UINotifications::Tasks::TasksRunningLong.new(report).deliver! 15 | TasksMailer.long_tasks(report).deliver_now 16 | end 17 | 18 | def humanized_name 19 | _('Deliver notifications about long running tasks') 20 | end 21 | 22 | def rescue_strategy_for_self 23 | ::Dynflow::Action::Rescue::Skip 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/TasksCardsGrid.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import { 4 | TASKS_DASHBOARD_AVAILABLE_TIMES, 5 | TASKS_DASHBOARD_AVAILABLE_QUERY_STATES, 6 | } from '../../TasksDashboardConstants'; 7 | import { MOCKED_DATA } from './TasksCardsGrid.fixtures'; 8 | import TasksCardsGrid from './TasksCardsGrid'; 9 | 10 | const fixtures = { 11 | 'render with minimal props': {}, 12 | 'render with props': { 13 | time: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK, 14 | query: { state: TASKS_DASHBOARD_AVAILABLE_QUERY_STATES.RUNNING }, 15 | data: MOCKED_DATA, 16 | updateQuery: jest.fn(), 17 | }, 18 | }; 19 | 20 | describe('TasksCardsGrid', () => 21 | testComponentSnapshotsWithFixtures(TasksCardsGrid, fixtures)); 22 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import TaskDetails from '../TaskDetails'; 4 | import { minProps } from './TaskDetails.fixtures'; 5 | 6 | const fixtures = { 7 | 'render with loading Props': { ...minProps, isLoading: true }, 8 | 'render with error Props': { 9 | ...minProps, 10 | status: 'ERROR', 11 | APIerror: { message: 'some-error' }, 12 | }, 13 | 'render with min Props': minProps, 14 | }; 15 | 16 | delete window.location; 17 | window.location = new URL( 18 | 'https://foreman.com/foreman_tasks/tasks/a15dd820-32f1-4ced-9ab7-c0fab8234c47/' 19 | ); 20 | 21 | describe('TaskDetails', () => { 22 | describe('rendering', () => 23 | testComponentSnapshotsWithFixtures(TaskDetails, fixtures)); 24 | }); 25 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/PausedTasksCard/PausedTasksCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { translate as __ } from 'foremanReact/common/I18n'; 3 | 4 | import TasksDonutCard from '../TasksDonutCard/TasksDonutCard'; 5 | 6 | const PausedTasksCard = ({ ...props }) => ( 7 | 13 | ); 14 | 15 | const filterUnwantedFields = obj => { 16 | const { title, wantedQueryState, ...newObj } = obj; 17 | return newObj; 18 | }; 19 | 20 | PausedTasksCard.propTypes = filterUnwantedFields(TasksDonutCard.propTypes); 21 | PausedTasksCard.defaultProps = filterUnwantedFields( 22 | TasksDonutCard.defaultProps 23 | ); 24 | 25 | export default PausedTasksCard; 26 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | import { STATUS } from 'foremanReact/constants'; 3 | import Task from '../Task'; 4 | 5 | const fixtures = { 6 | 'render with minimal Props': { 7 | id: 'test', 8 | taskReloadStart: jest.fn(), 9 | taskProgressToggle: jest.fn(), 10 | }, 11 | 'render with some Props': { 12 | id: 'test', 13 | state: 'paused', 14 | hasSubTasks: true, 15 | dynflowEnableConsole: true, 16 | parentTask: 'parent-id', 17 | taskReload: true, 18 | canEdit: true, 19 | status: STATUS.RESOLVED, 20 | taskProgressToggle: jest.fn(), 21 | taskReloadStart: jest.fn(), 22 | }, 23 | }; 24 | 25 | describe('Task rendering', () => 26 | testComponentSnapshotsWithFixtures(Task, fixtures)); 27 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/RunningTasksCard/RunningTasksCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { translate as __ } from 'foremanReact/common/I18n'; 3 | 4 | import TasksDonutCard from '../TasksDonutCard/TasksDonutCard'; 5 | 6 | const RunningTasksCard = ({ ...props }) => ( 7 | 13 | ); 14 | 15 | const filterUnwantedFields = obj => { 16 | const { title, wantedQueryState, ...newObj } = obj; 17 | return newObj; 18 | }; 19 | 20 | RunningTasksCard.propTypes = filterUnwantedFields(TasksDonutCard.propTypes); 21 | RunningTasksCard.defaultProps = filterUnwantedFields( 22 | TasksDonutCard.defaultProps 23 | ); 24 | 25 | export default RunningTasksCard; 26 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Raw.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import Raw from '../Raw'; 4 | 5 | const fixtures = { 6 | 'render without Props': {}, 7 | 'render with Props': { 8 | startedAt: '2019-06-17 16:04:09 +0300', 9 | endedAt: '2019-06-17 16:05:09 +0300', 10 | id: '6b0d6db2-e9ab-40da-94e5-b6842ac50bd0', 11 | label: 'Actions::Katello::EventQueue::Monitor', 12 | input: { 13 | locale: 'en', 14 | current_request_id: 1, 15 | current_user_id: 4, 16 | current_organization_id: 2, 17 | current_location_id: 3, 18 | }, 19 | output: {}, 20 | externalId: 'test', 21 | }, 22 | }; 23 | 24 | describe('Raw', () => { 25 | describe('rendering', () => 26 | testComponentSnapshotsWithFixtures(Raw, fixtures)); 27 | }); 28 | -------------------------------------------------------------------------------- /lib/foreman_tasks/triggers.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | module Triggers 3 | # for test overrides if needed 4 | attr_writer :foreman_tasks 5 | def foreman_tasks 6 | @foreman_tasks ||= ForemanTasks 7 | end 8 | 9 | def trigger(action, *args, &block) 10 | foreman_tasks.trigger action, *args, &block 11 | end 12 | 13 | def trigger_task(async, action, *args, &block) 14 | foreman_tasks.trigger_task(async, action, *args, &block) 15 | end 16 | 17 | def async_task(action, *args, &block) 18 | foreman_tasks.async_task(action, *args, &block) 19 | end 20 | 21 | def sync_task(action, *args, &block) 22 | foreman_tasks.sync_task(action, *args, &block) 23 | end 24 | 25 | def delay(action, delay_options, *args) 26 | foreman_tasks.delay(action, delay_options, *args) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/graphql/queries/task_query_test.rb: -------------------------------------------------------------------------------- 1 | require 'foreman_tasks_test_helper' 2 | 3 | module Queries 4 | class TaskQueryTest < GraphQLQueryTestCase 5 | let(:query) do 6 | <<-GRAPHQL 7 | query ( 8 | $id: String! 9 | ) { 10 | task(id: $id) { 11 | id 12 | action 13 | result 14 | } 15 | } 16 | GRAPHQL 17 | end 18 | 19 | let(:res) { 'inconclusive' } 20 | let(:task) { FactoryBot.create(:some_task, :result => res) } 21 | 22 | let(:global_id) { Foreman::GlobalId.for(task) } 23 | let(:variables) { { id: global_id } } 24 | let(:data) { result['data']['task'] } 25 | 26 | test 'should fetch task data' do 27 | assert_empty result['errors'] 28 | 29 | assert_equal global_id, data['id'] 30 | assert_equal task.result, data['result'] 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/TasksDashboard.scss: -------------------------------------------------------------------------------- 1 | @import 'foremanReact/common/variables'; 2 | @import 'foremanReact/common/scss/mixins'; 3 | 4 | @mixin create-tasks-dashboard-column($columns: 12, $screen-min: 0, $gutter: $grid-gutter-width) { 5 | @media (min-width: $screen-min) { 6 | width: percentage(($columns / $grid-columns)); 7 | float: left; 8 | position: relative; 9 | min-height: 1px; 10 | padding-right: ($gutter / 2); 11 | padding-left: ($gutter / 2); 12 | } 13 | } 14 | 15 | .tasks-dashboard-grid { 16 | min-height: 200px; 17 | background-color: #f5f5f5; 18 | margin: 5px -60px 20px -60px; 19 | padding: 20px 50px; 20 | 21 | .row > div { 22 | @include create-tasks-dashboard-column(12, 0); 23 | @include create-tasks-dashboard-column(6, 900px); 24 | @include create-tasks-dashboard-column(3, 1500px); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/graphql/types/task.rb: -------------------------------------------------------------------------------- 1 | module Types 2 | class Task < Types::BaseObject 3 | description 'A Task' 4 | model_class ::ForemanTasks::Task 5 | 6 | global_id_field :id 7 | field :type, String 8 | field :label, String 9 | field :started_at, GraphQL::Types::ISO8601DateTime 10 | field :ended_at, GraphQL::Types::ISO8601DateTime 11 | field :state, String 12 | field :result, String 13 | field :external_id, String 14 | field :parent_task_id, String 15 | field :start_at, GraphQL::Types::ISO8601DateTime 16 | field :start_before, GraphQL::Types::ISO8601DateTime 17 | field :action, String 18 | field :user_id, Integer 19 | field :state_updated_at, GraphQL::Types::ISO8601DateTime 20 | 21 | def self.graphql_definition 22 | super.tap { |type| type.instance_variable_set(:@name, 'ForemanTasks::Task') } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/graphql/queries/recurring_logic_test.rb: -------------------------------------------------------------------------------- 1 | require 'foreman_tasks_test_helper' 2 | 3 | module Queries 4 | class RecurringLogicTest < GraphQLQueryTestCase 5 | let(:query) do 6 | <<-GRAPHQL 7 | query($id: String!) { 8 | recurringLogic(id: $id) { 9 | id 10 | cronLine 11 | } 12 | } 13 | GRAPHQL 14 | end 15 | 16 | let(:cron_line) { '5 4 3 2 1' } 17 | let(:recurring_logic) { FactoryBot.create(:recurring_logic, :cron_line => cron_line) } 18 | let(:global_id) { Foreman::GlobalId.for(recurring_logic) } 19 | let(:variables) { { id: global_id } } 20 | let(:data) { result['data']['recurringLogic'] } 21 | 22 | test "should fetch recurring logic" do 23 | assert_empty result['errors'] 24 | assert_equal global_id, data['id'] 25 | assert_equal cron_line, data['cronLine'] 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/lib/actions/middleware/inherit_task_groups.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | module Middleware 3 | class InheritTaskGroups < Dynflow::Middleware 4 | def delay(*args) 5 | pass(*args) 6 | end 7 | 8 | def plan(*args) 9 | inherit_task_groups 10 | pass(*args) 11 | end 12 | 13 | def run(*args) 14 | pass(*args) 15 | collect_children_task_groups 16 | end 17 | 18 | def finalize 19 | pass 20 | end 21 | 22 | private 23 | 24 | def inherit_task_groups 25 | task.add_missing_task_groups(task.parent_task.task_groups) if task.parent_task 26 | end 27 | 28 | def collect_children_task_groups 29 | task.add_missing_task_groups task.sub_tasks.map(&:task_groups).flatten 30 | end 31 | 32 | def task 33 | @task ||= action.task 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/tasks/dashboard/_tasks_status.html.erb: -------------------------------------------------------------------------------- 1 |

<%=_("Task Status")%>

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <% ForemanTasks::Task::Summarizer.new.summarize_by_status.each do |result| %> 10 | 11 | 12 | 13 | 14 | 15 | 16 | <% end %> 17 |
<%= _("State") %><%= _("Result") %><%= _("No. of Tasks") %><%= _("Last start time") %>
<%= result.state %><%= result.result %><%= link_to result.count, main_app.foreman_tasks_tasks_path(:search => "state=#{result.state}&result=#{result.result}") %><%= result.started_at ? date_time_relative(result.started_at) : _('N/A') %>
18 | -------------------------------------------------------------------------------- /app/lib/actions/middleware/rails_executor_wrap.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | module Middleware 3 | # In development with Rails auto-reloading and using `sync_task` method, 4 | # it could lead to dead-locking due to the Rails main thread locking the 5 | # the class loader. 6 | # 7 | # This middleware marks the part of the code that can 8 | # use the auto-loader so that Rails know they should avoid the locking there. 9 | # See https://github.com/ruby-concurrency/concurrent-ruby/issues/585#issuecomment-256131537 10 | # for more details. 11 | class RailsExecutorWrap < Dynflow::Middleware 12 | def run(*args) 13 | ::Rails.application.executor.wrap do 14 | pass(*args) 15 | end 16 | end 17 | 18 | def finalize 19 | ::Rails.application.executor.wrap do 20 | pass 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /deploy/foreman-tasks.sysconfig: -------------------------------------------------------------------------------- 1 | FOREMAN_USER=foreman 2 | BUNDLER_EXT_HOME=/usr/share/foreman 3 | RAILS_ENV=production 4 | FOREMAN_LOGGING=warn 5 | FOREMAN_LOGGING_SQL=warn 6 | FOREMAN_TASK_PARAMS="-p foreman" 7 | FOREMAN_LOG_DIR=/var/log/foreman 8 | 9 | RUBY_GC_MALLOC_LIMIT=4000100 10 | RUBY_GC_MALLOC_LIMIT_MAX=16000100 11 | RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR=1.1 12 | RUBY_GC_OLDMALLOC_LIMIT=16000100 13 | RUBY_GC_OLDMALLOC_LIMIT_MAX=16000100 14 | 15 | #Set the number of executors you want to run 16 | #EXECUTORS_COUNT=1 17 | 18 | #Set memory limit for executor process, before it's restarted automatically 19 | #EXECUTOR_MEMORY_LIMIT=2gb 20 | 21 | #Set delay before first memory polling to let executor initialize (in sec) 22 | #EXECUTOR_MEMORY_MONITOR_DELAY=7200 #default: 2 hours 23 | 24 | #Set memory polling interval, process memory will be checked every N seconds. 25 | #EXECUTOR_MEMORY_MONITOR_INTERVAL=60 26 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/Components/__test__/SelectAllAlert.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import { SelectAllAlert } from '../SelectAllAlert'; 4 | 5 | const baseProps = { 6 | itemCount: 7, 7 | perPage: 5, 8 | selectAllRows: jest.fn(), 9 | unselectAllRows: jest.fn(), 10 | }; 11 | const fixtures = { 12 | 'renders SelectAllAlert with perPage > itemCout': { 13 | allRowsSelected: false, 14 | itemCount: 7, 15 | perPage: 10, 16 | ...baseProps, 17 | }, 18 | 'renders SelectAllAlert without all rows selected': { 19 | allRowsSelected: false, 20 | ...baseProps, 21 | }, 22 | 'renders SelectAllAlert with all rows selected': { 23 | allRowsSelected: true, 24 | ...baseProps, 25 | }, 26 | }; 27 | 28 | describe('SelectAllAlert', () => 29 | testComponentSnapshotsWithFixtures(SelectAllAlert, fixtures)); 30 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js: -------------------------------------------------------------------------------- 1 | import { translate as __, documentLocale } from 'foremanReact/common/I18n'; 2 | import { isoCompatibleDate } from 'foremanReact/common/helpers'; 3 | import humanizeDuration from 'humanize-duration'; 4 | 5 | export const durationInWords = ( 6 | start, 7 | finish, 8 | selectedLocale = documentLocale() 9 | ) => { 10 | if (!start) return __('N/A'); 11 | start = new Date(isoCompatibleDate(start)).getTime(); 12 | finish = new Date(isoCompatibleDate(finish)).getTime(); 13 | return { 14 | text: humanizeDuration(new Date(finish - start).getTime(), { 15 | largest: 1, 16 | language: selectedLocale, 17 | fallbacks: ['en'], 18 | }), 19 | tooltip: `${numberWithDelimiter((finish - start) / 1000)} ${__('seconds')}`, 20 | }; 21 | }; 22 | 23 | const numberWithDelimiter = x => 24 | x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); 25 | -------------------------------------------------------------------------------- /app/views/tasks_mailer/long_tasks.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <%= _("Tasks lingering in states %{states} since %{time}") % { 3 | time: @report.time - @report.interval, 4 | states: @report.states.join(', ') 5 | } %> 6 |

7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @report.tasks.each do |task| %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% end %> 26 |
<%= _("ID") %><%= _("Action") %><%= _("Label") %><%= _("State") %><%= _("State updated at") %>
<%= link_to task.id, foreman_tasks_task_url(task) %><%= task.action %><%= task.label %><%= task.state %><%= task.state_updated_at %>
27 |
28 | 29 | <%= link_to _('More details'), foreman_tasks_tasks_url(search: @report.query) %> 30 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/PausedTasksCard/__snapshots__/PausedTasksCard.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PausedTasksCard render with minimal props 1`] = ` 4 | 19 | `; 20 | 21 | exports[`PausedTasksCard render with some props 1`] = ` 22 | 38 | `; 39 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/RunningTasksCard/__snapshots__/RunningTasksCard.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RunningTasksCard render with minimal props 1`] = ` 4 | 19 | `; 20 | 21 | exports[`RunningTasksCard render with some props 1`] = ` 22 | 38 | `; 39 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTableActions.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TasksTable actions handles openClickedModal 1`] = ` 4 | Object { 5 | "payload": Object { 6 | "clicked": Object { 7 | "taskId": "some-id", 8 | "taskName": "some-name", 9 | }, 10 | }, 11 | "type": "UPDATE_CLICKED", 12 | } 13 | `; 14 | 15 | exports[`TasksTable actions handles openModalAction 1`] = ` 16 | Object { 17 | "payload": Object { 18 | "modalID": "some-modal-id", 19 | }, 20 | "type": "UPDATE_MODAL", 21 | } 22 | `; 23 | 24 | exports[`TasksTable actions should selectPage and succeed 1`] = ` 25 | Array [ 26 | Array [ 27 | Object { 28 | "payload": Array [ 29 | "some-id", 30 | ], 31 | "type": "SELECT_ROWS", 32 | }, 33 | ], 34 | Array [ 35 | Object { 36 | "type": "OPEN_SELECT_ALL", 37 | }, 38 | ], 39 | ] 40 | `; 41 | -------------------------------------------------------------------------------- /app/lib/actions/middleware/load_setting_values.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | module Middleware 3 | class LoadSettingValues < ::Dynflow::Middleware 4 | # ::Actions::Middleware::LoadSettingValues 5 | # 6 | # A middleware to ensure we load current setting values 7 | 8 | def delay(*args) 9 | reload_setting_values 10 | pass(*args) 11 | end 12 | 13 | def plan(*args) 14 | reload_setting_values 15 | pass(*args) 16 | end 17 | 18 | def run(*args) 19 | reload_setting_values 20 | pass(*args) 21 | end 22 | 23 | def finalize(*args) 24 | reload_setting_values 25 | pass(*args) 26 | end 27 | 28 | def hook(*args) 29 | reload_setting_values 30 | pass(*args) 31 | end 32 | 33 | private 34 | 35 | def reload_setting_values 36 | ::Foreman.settings.load_values 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/lib/actions/helpers/lifecycle_logging.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | module Helpers 3 | module LifecycleLogging 4 | def self.included(base) 5 | base.execution_plan_hooks.use :log_task_state_change 6 | end 7 | 8 | def log_task_state_change(execution_plan) 9 | return unless root_action? 10 | logger = ::Rails.application.dynflow.world.action_logger 11 | task_id = ForemanTasks::Task::DynflowTask.where(external_id: execution_plan.id).pluck(:id).first 12 | 13 | task_id_parts = [] 14 | task_id_parts << "id: #{task_id}" if task_id 15 | task_id_parts << "execution_plan_id: #{execution_plan.id}" 16 | result_info = " result: #{execution_plan.result}" if [:stopped, :paused].include?(execution_plan.state) 17 | logger.info("Task {label: #{execution_plan.label}, #{task_id_parts.join(', ')}} state changed: #{execution_plan.state} #{result_info}") 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskActions/TaskActionsConstants.js: -------------------------------------------------------------------------------- 1 | export const UNLOCK_MODAL = 'unlockModal'; 2 | export const FORCE_UNLOCK_MODAL = 'forceUnlockModal'; 3 | 4 | export const TASKS_RESUME_REQUEST = 'TASKS_RESUME_REQUEST'; 5 | export const TASKS_RESUME_SUCCESS = 'TASKS_RESUME_SUCCESS'; 6 | export const TASKS_RESUME_FAILURE = 'TASKS_RESUME_FAILURE'; 7 | export const TASKS_CANCEL_REQUEST = 'TASKS_CANCEL_REQUEST'; 8 | export const TASKS_CANCEL_SUCCESS = 'TASKS_CANCEL_SUCCESS'; 9 | export const TASKS_CANCEL_FAILURE = 'TASKS_CANCEL_FAILURE'; 10 | 11 | export const TASKS_FORCE_CANCEL_REQUEST = 'TASKS_FORCE_CANCEL_REQUEST'; 12 | export const TASKS_FORCE_CANCEL_SUCCESS = 'TASKS_FORCE_CANCEL_SUCCESS'; 13 | export const TASKS_FORCE_CANCEL_FAILURE = 'TASKS_FORCE_CANCEL_FAILURE'; 14 | export const TASKS_UNLOCK_REQUEST = 'TASKS_UNLOCK_REQUEST'; 15 | export const TASKS_UNLOCK_SUCCESS = 'TASKS_UNLOCK_SUCCESS'; 16 | export const TASKS_UNLOCK_FAILURE = 'TASKS_UNLOCK_FAILURE'; 17 | -------------------------------------------------------------------------------- /db/migrate/20181206131627_make_locks_exclusive.foreman_tasks.rb: -------------------------------------------------------------------------------- 1 | class MakeLocksExclusive < ActiveRecord::Migration[5.0] 2 | BATCH_SIZE = 2 3 | 4 | def up 5 | change_table :foreman_tasks_locks do |t| 6 | t.remove :exclusive 7 | t.remove :name 8 | t.remove_index :name => 'index_foreman_tasks_locks_on_resource_type_and_resource_id' 9 | t.index [:resource_type, :resource_id], :unique => true 10 | end 11 | end 12 | 13 | def down 14 | change_table :foreman_tasks_locks do |t| 15 | t.boolean :exclusive, index: true 16 | t.string :name, index: true 17 | t.remove_index :name => 'index_foreman_tasks_locks_on_resource_type_and_resource_id' 18 | t.index [:resource_type, :resource_id] 19 | end 20 | 21 | scope = ForemanTasks::Lock.where(:name => nil) 22 | while scope.limit(BATCH_SIZE).update_all(:name => 'lock') == BATCH_SIZE; end 23 | change_column_null(:foreman_tasks_locks, :name, false) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/Components/TableSelectionCell.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Table } from 'patternfly-react'; 4 | import { translate as __ } from 'foremanReact/common/I18n'; 5 | import { noop } from 'foremanReact/common/helpers'; 6 | 7 | const TableSelectionCell = ({ id, label, checked, onChange, ...props }) => ( 8 | 9 | 16 | 17 | ); 18 | 19 | TableSelectionCell.propTypes = { 20 | id: PropTypes.string.isRequired, 21 | label: PropTypes.string, 22 | checked: PropTypes.bool, 23 | onChange: PropTypes.func, 24 | }; 25 | 26 | TableSelectionCell.defaultProps = { 27 | label: __('Select row'), 28 | checked: false, 29 | onChange: noop, 30 | }; 31 | 32 | export default TableSelectionCell; 33 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/api/tasks/details.json.rabl: -------------------------------------------------------------------------------- 1 | object @task if @task 2 | 3 | extends 'foreman_tasks/api/tasks/show' 4 | 5 | attributes :parent_task_id, :start_at, :start_before, :external_id 6 | node(:action) { @task.action } 7 | node(:execution_plan) { { state: @task.execution_plan.state, cancellable: @task.execution_plan.cancellable? } } 8 | node(:failed_steps) { @task.input_output_failed_steps } 9 | node(:running_steps) { @task.input_output_running_steps } 10 | node(:help) { troubleshooting_info_text } 11 | node(:has_sub_tasks) { @task.sub_tasks.any? } 12 | 13 | node(:locks) do 14 | @task.locks.map { |lock| partial('foreman_tasks/api/locks/show', :object => lock) } 15 | end 16 | node(:links) do 17 | @task.links.map { |link| partial('foreman_tasks/api/locks/show', :object => link, :locals => { :link => true }) } 18 | end 19 | node(:username_path) { username_link_task(@task.owner, @task.username) } 20 | node(:dynflow_enable_console) { Setting['dynflow_enable_console'] } 21 | -------------------------------------------------------------------------------- /db/migrate/20181206123910_create_foreman_tasks_links.foreman_tasks.rb: -------------------------------------------------------------------------------- 1 | class CreateForemanTasksLinks < ActiveRecord::Migration[4.2] 2 | def change 3 | # rubocop:disable Rails/CreateTableWithTimestamps 4 | create_table :foreman_tasks_links do |t| 5 | task_id_options = { :index => true, :null => true } 6 | if on_postgresql? 7 | t.uuid :task_id, task_id_options 8 | else 9 | t.string :task_id, task_id_options 10 | end 11 | t.string :resource_type 12 | t.integer :resource_id 13 | end 14 | # rubocop:enable Rails/CreateTableWithTimestamps 15 | 16 | add_index :foreman_tasks_links, [:resource_type, :resource_id] 17 | add_index :foreman_tasks_links, [:task_id, :resource_type, :resource_id], 18 | :unique => true, :name => 'foreman_tasks_links_unique_index' 19 | end 20 | 21 | private 22 | 23 | def on_postgresql? 24 | ActiveRecord::Base.connection.adapter_name.casecmp('postgresql').zero? 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/ConfirmModalSelectors.js: -------------------------------------------------------------------------------- 1 | import { 2 | selectTasksTableQuery, 3 | selectResults, 4 | selectSelectedRows, 5 | selectItemCount, 6 | selectAllRowsSelected, 7 | } from '../../TasksTableSelectors'; 8 | 9 | export const selectClicked = state => 10 | selectTasksTableQuery(state).clicked || {}; 11 | 12 | export const selectSelectedTasks = state => { 13 | const selectedIDs = selectResults(state).filter(item => 14 | selectSelectedRows(state).includes(item.id) 15 | ); 16 | return selectedIDs.map(item => ({ 17 | name: item.action, 18 | id: item.id, 19 | isCancellable: item.availableActions.cancellable, 20 | isResumable: item.availableActions.resumable, 21 | canEdit: item.canEdit, 22 | })); 23 | }; 24 | 25 | export const selectSelectedRowsLen = state => { 26 | if (selectAllRowsSelected(state)) { 27 | return selectItemCount(state); 28 | } 29 | return selectSelectedRows(state).length; 30 | }; 31 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/Components/TableSelectionHeaderCell.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Table } from 'patternfly-react'; 4 | import { noop } from 'foremanReact/common/helpers'; 5 | 6 | const TableSelectionHeaderCell = ({ 7 | id, 8 | label, 9 | checked, 10 | onChange, 11 | ...props 12 | }) => ( 13 | 14 | 21 | 22 | ); 23 | 24 | TableSelectionHeaderCell.propTypes = { 25 | id: PropTypes.string, 26 | label: PropTypes.string, 27 | checked: PropTypes.bool, 28 | onChange: PropTypes.func, 29 | }; 30 | 31 | TableSelectionHeaderCell.defaultProps = { 32 | id: 'selectAll', 33 | label: '', 34 | checked: false, 35 | onChange: noop, 36 | }; 37 | 38 | export default TableSelectionHeaderCell; 39 | -------------------------------------------------------------------------------- /test/unit/tasks_setting_test.rb: -------------------------------------------------------------------------------- 1 | require 'foreman_tasks_test_helper' 2 | 3 | class TasksSettingTest < ActiveSupport::TestCase 4 | describe "foreman_tasks_troubleshooting_url" do 5 | test "invalid url for foreman_tasks_troubleshooting_url are rejected" do 6 | url_setting = Setting.new(name: 'foreman_tasks_troubleshooting_url', value: 'invalid url') 7 | url_setting.valid? 8 | rescue => e 9 | assert_includes e.message, "Invalid URL" 10 | end 11 | 12 | test "valid foreman_tasks_troubleshooting_url values are accepted" do 13 | url_setting = Setting.new(name: 'foreman_tasks_troubleshooting_url', value: 'https://example.com') 14 | assert url_setting.valid? 15 | end 16 | 17 | test "valid foreman_tasks_troubleshooting_url values with placeholder are accepted" do 18 | url_setting = Setting.new(name: 'foreman_tasks_troubleshooting_url', value: 'https://example/%{label}/%{version}.com') 19 | assert url_setting.valid? 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /script/npm_link_foreman_js.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script replace the npm installation of `foreman-js` 4 | # with your local version. Usefull when developing `foreman-js` 5 | # Read more about foreman-js: https://github.com/theforeman/foreman-js 6 | # 7 | # This script designed to run using `npm run foreman-js:link` in foreman root 8 | 9 | set -e 10 | 11 | if [[ -z "${FOREMAN_JS_LOCATION}" ]]; then # FOREMAN_JS_LOCATION is empty 12 | FOREMAN_JS_LOCATION="../foreman-js" 13 | echo "FOREMAN_JS_LOCATION is not defined, using \"${FOREMAN_JS_LOCATION}\" instead" 14 | elif [ ! -d "${FOREMAN_JS_LOCATION}" ]; then 15 | echo "Can't find folder ${FOREMAN_JS_LOCATION}" 16 | exit 1 17 | fi 18 | 19 | FOREMAN_JS_LOCATION="../${FOREMAN_JS_LOCATION}" 20 | FOREMAN_JS_PACKAGES_LOCATION="${FOREMAN_JS_LOCATION}/packages" 21 | FOREMAN_JS_INSTALL_LOCATION="./node_modules/@theforeman" 22 | 23 | set -x 24 | 25 | rm -rf $FOREMAN_JS_INSTALL_LOCATION 26 | ln -s $FOREMAN_JS_PACKAGES_LOCATION $FOREMAN_JS_INSTALL_LOCATION 27 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Routes/__snapshots__/ForemanTasksRoutes.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ForemanTasksRoutes should create routes 1`] = ` 4 | Object { 5 | "indexTasks": Object { 6 | "exact": true, 7 | "path": "/foreman_tasks/tasks", 8 | "render": [Function], 9 | "renderResult": , 13 | }, 14 | "showTask": Object { 15 | "path": "/foreman_tasks/ex_tasks/:id", 16 | "render": [Function], 17 | "renderResult": , 21 | }, 22 | "subTasks": Object { 23 | "exact": true, 24 | "path": "/foreman_tasks/tasks/:id/sub_tasks", 25 | "render": [Function], 26 | "renderResult": , 35 | }, 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksTimeRow/__snapshots__/TasksTimeRow.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TasksTimeRow render with minimal props 1`] = ` 4 | 9 | 12 | With focus on last 13 | 14 | 20 | 21 | `; 22 | 23 | exports[`TasksTimeRow render with props 1`] = ` 24 | 29 | 32 | With focus on last 33 | 34 | 40 | 41 | `; 42 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/Components/ConfirmModal/__test__/__snapshots__/ConfirmModalSelectors.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TasksDashboard - Selectors should select clicked 1`] = ` 4 | Object { 5 | "taskId": "1", 6 | "taskName": "test-task", 7 | } 8 | `; 9 | 10 | exports[`TasksDashboard - Selectors should select selectedRowsLen 1 1`] = `3`; 11 | 12 | exports[`TasksDashboard - Selectors should select selectedRowsLen all 1`] = `3`; 13 | 14 | exports[`TasksDashboard - Selectors should select selectedRowsLen some 1`] = `0`; 15 | 16 | exports[`TasksDashboard - Selectors should select selectedTasks 1`] = ` 17 | Array [ 18 | Object { 19 | "canEdit": true, 20 | "id": 1, 21 | "isCancellable": true, 22 | "isResumable": undefined, 23 | "name": "action1", 24 | }, 25 | Object { 26 | "canEdit": undefined, 27 | "id": 2, 28 | "isCancellable": undefined, 29 | "isResumable": true, 30 | "name": "action2", 31 | }, 32 | ] 33 | `; 34 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksIndexPage.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TasksIndexPage rendering render with minimal props 1`] = ` 4 | 39 | `; 40 | -------------------------------------------------------------------------------- /app/lib/actions/middleware/recurring_logic.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | module Middleware 3 | class RecurringLogic < ::Dynflow::Middleware 4 | # ::Actions::Middleware::RecurringLogic 5 | # 6 | # A middleware designed to make action repeatable. 7 | # After an action is delayed, it checks whether the delay_options 8 | # hash contains an id of a recurring logic. If so, it adds the task 9 | # to the recurring logic's task group, otherwise does nothing. 10 | def delay(delay_options, *args) 11 | pass(delay_options, *args).tap do 12 | if delay_options[:recurring_logic_id] 13 | task.add_missing_task_groups(recurring_logic(delay_options).task_group) 14 | end 15 | end 16 | end 17 | 18 | private 19 | 20 | def recurring_logic(delay_options) 21 | ForemanTasks::RecurringLogic.find(delay_options[:recurring_logic_id]) 22 | end 23 | 24 | def task 25 | @task ||= action.task 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/foreman_tasks/concerns/parameters/triggering.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | module Concerns 3 | module Parameters 4 | module Triggering 5 | extend ActiveSupport::Concern 6 | 7 | class_methods do 8 | def triggering_params_filter 9 | Foreman::ParameterFilter.new(::ForemanTasks::Triggering).tap do |filter| 10 | filter.permit_by_context( 11 | [ 12 | :mode, 13 | :start_at, 14 | :start_before, 15 | *::ForemanTasks::Triggering::PARAMS, 16 | :days_of_week => {}, 17 | :time => {}, 18 | :end_time => {}, 19 | ], 20 | :nested => true 21 | ) 22 | end 23 | end 24 | end 25 | 26 | def triggering_params 27 | self.class.triggering_params_filter.filter_params(params, parameter_filter_context, :triggering) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/views/foreman_tasks/tasks/dashboard/_latest_tasks_in_error_warning.html.erb: -------------------------------------------------------------------------------- 1 |

<%= link_to _("Latest Warning/Error Tasks"), foreman_tasks_tasks_path(:order=>'started_at DESC') %>

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <% ForemanTasks::Task::Summarizer.new.latest_tasks_in_errors_warning.each do |task| %> 10 | 11 | 12 | 13 | 14 | 15 | 16 | <% end %> 17 |
<%= _("Name") %><%= _("State") %><%= _("Result") %><%= _("Started") %>
<%= link_to task.humanized[:action], defined?(main_app) ? main_app.foreman_tasks_task_path(task.id) : foreman_tasks_task_path(task.id) %><%= task.state %><%= task.result %><%= task.started_at ? date_time_relative(task.started_at) : _('N/A') %>
18 | -------------------------------------------------------------------------------- /app/lib/actions/helpers/lock.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | module Helpers 3 | module Lock 4 | # @see Lock.exclusive! 5 | def exclusive_lock!(resource) 6 | phase! Dynflow::Action::Plan 7 | parent_lock = ::ForemanTasks::Lock.for_resource(resource).where(:task_id => task.self_and_parents.map(&:id)).first 8 | if parent_lock 9 | ForemanTasks::Link.link_resource_and_related!(resource, task) 10 | parent_lock 11 | else 12 | ::ForemanTasks::Lock.exclusive!(resource, task) 13 | end 14 | end 15 | 16 | # @see Lock.lock! 17 | def lock!(resource, *_lock_names) 18 | Foreman::Deprecation.deprecation_warning('2.4', 'locking in foreman-tasks was reworked, please use a combination of exclusive_lock! and link! instead.') 19 | exclusive_lock!(resource) 20 | end 21 | 22 | # @see Lock.link! 23 | def link!(resource) 24 | phase! Dynflow::Action::Plan 25 | ::ForemanTasks::Link.link!(resource, task) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /db/migrate/20181206131436_drop_old_locks.foreman_tasks.rb: -------------------------------------------------------------------------------- 1 | class DropOldLocks < ActiveRecord::Migration[5.0] 2 | BATCH_SIZE = 10_000 3 | 4 | # Delete all locks which are exclusive or have a stopped task or are orphaned 5 | def up 6 | scope = ForemanTasks::Lock.left_outer_joins(:task) 7 | scope = scope.where(:exclusive => false) 8 | .or(scope.where("#{ForemanTasks::Task.table_name}.state" => ['stopped', nil])) 9 | scope.limit(BATCH_SIZE).delete_all while scope.any? 10 | 11 | # For each group of locks, where each lock has the same task_id, resource_type and resource_id 12 | # return the highest id within the group, if there is more than 1 lock in the group 13 | scope = ForemanTasks::Lock.select("MAX(id) as id") 14 | .group(:task_id, :resource_type, :resource_id) 15 | .having("count(*) > 1") 16 | 17 | # Make sure there is at most one lock per task and resource 18 | ForemanTasks::Lock.where(:id => scope.limit(BATCH_SIZE)).delete_all while scope.any? 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/foreman_tasks/tasks/reschedule_long_running_tasks_checker.rake: -------------------------------------------------------------------------------- 1 | namespace :foreman_tasks do 2 | desc <<~DESC 3 | Reschedules the long running task checker recurring logic to run at a different schedule. ENV variables: 4 | 5 | * FOREMAN_TASKS_CHECK_LONG_RUNNING_TASKS_CRONLINE : A cron line describing the schedule, defaults to 0 0 * * * 6 | DESC 7 | task :reschedule_long_running_tasks_checker => ['environment', 'dynflow:client'] do 8 | User.as_anonymous_admin do 9 | task_class = Actions::CheckLongRunningTasks 10 | cronline = ENV['FOREMAN_TASKS_CHECK_LONG_RUNNING_TASKS_CRONLINE'] || '0 0 * * *' 11 | rl = ForemanTasks::RecurringLogic.joins(:tasks) 12 | .where(state: 'active') 13 | .merge(ForemanTasks::Task.where(label: task_class.name)) 14 | .first 15 | if rl&.cron_line != cronline 16 | rl.cancel 17 | ForemanTasks.register_scheduled_task(task_class, cronline) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/graphql/mutations/recurring_logics/cancel.rb: -------------------------------------------------------------------------------- 1 | module Mutations 2 | module RecurringLogics 3 | class Cancel < BaseMutation 4 | graphql_name 'CancelRecurringLogic' 5 | description 'Cancels recurring logic and all its active tasks' 6 | resource_class ::ForemanTasks::RecurringLogic 7 | 8 | argument :id, ID, required: true 9 | 10 | field :errors, [Types::AttributeError], null: false 11 | field :recurring_logic, Types::RecurringLogic, null: true 12 | 13 | def resolve(id:) 14 | recurring_logic = load_object_by(id: id) 15 | authorize!(recurring_logic, :edit) 16 | task_errors = [] 17 | begin 18 | recurring_logic.cancel 19 | rescue => e 20 | task_errors = [{ path: ['tasks'], message: "There has been an error when canceling one of the tasks: #{e}" }] 21 | end 22 | errors = recurring_logic.errors.any? ? map_errors_to_path(recurring_logic) : [] 23 | { recurring_logic: recurring_logic, errors: (errors + task_errors) } 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksLabelsRow/__snapshots__/TasksLabelsRow.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TasksLabelsRow rendering render with minimal props 1`] = ` 4 | 9 | 12 | Active Filters: 13 | 14 | 15 | `; 16 | 17 | exports[`TasksLabelsRow rendering render with props 1`] = ` 18 | 23 | 26 | Active Filters: 27 | 28 | 36 | 46 | 47 | `; 48 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/TasksTable.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TasksTable rendering render with error Props 1`] = ` 4 | 9 | `; 10 | 11 | exports[`TasksTable rendering render with loading Props 1`] = ` 12 |
15 | 23 | 26 | 27 | `; 28 | 29 | exports[`TasksTable rendering render with minimal Props 1`] = ` 30 |
33 |
42 | 45 | 46 | `; 47 | 48 | exports[`TasksTable rendering render with no results 1`] = ` 49 | 50 | No Tasks 51 | 52 | `; 53 | -------------------------------------------------------------------------------- /app/lib/actions/helpers/with_continuous_output.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | module Helpers 3 | module WithContinuousOutput 4 | # @override 5 | # array of objects defining fill_continuous_input 6 | def continuous_output_providers 7 | [] 8 | end 9 | 10 | def continuous_output 11 | continuous_output = ::ForemanTasks::ContinuousOutput.new 12 | continuous_output_providers.each do |continous_output_provider| 13 | continous_output_provider.fill_continuous_output(continuous_output) 14 | end 15 | continuous_output 16 | end 17 | 18 | def fill_planning_errors_to_continuous_output(continuous_output) 19 | execution_plan.errors.map do |e| 20 | case e.exception 21 | when ::Actions::ProxyAction::ProxyActionMissing 22 | continuous_output.add_output(e.message, 'debug', task.started_at) 23 | else 24 | continuous_output.add_exception(_('Failed to initialize'), e.exception, task.started_at) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/support/dummy_dynflow_action.rb: -------------------------------------------------------------------------------- 1 | module Support 2 | class DummyDynflowAction < Actions::EntryAction 3 | end 4 | 5 | class DummyPauseAction < Actions::EntryAction 6 | def plan 7 | plan_action(DummyPauseActionWithCustomTroubleshooting) 8 | plan_self 9 | end 10 | 11 | def run 12 | error! "This is an error" 13 | end 14 | end 15 | 16 | class DummyPauseActionWithCustomTroubleshooting < Actions::EntryAction 17 | def run 18 | error! "This is an error" 19 | end 20 | 21 | def troubleshooting_info 22 | ForemanTasks::TroubleshootingHelpGenerator::Info.new.tap do |i| 23 | i.add_line _('This task requires special handling.') 24 | i.add_link(ForemanTasks::TroubleshootingHelpGenerator::Link.new( 25 | name: :custom_link, 26 | title: _('custom link'), 27 | href: "/additional_troubleshooting_page", 28 | description: _("Investigate %{link} on more details for this custom error.") 29 | )) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/RunningSteps.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import RunningSteps from '../RunningSteps'; 4 | 5 | const minProps = { 6 | id: 'task-id1', 7 | taskReload: true, 8 | cancelStep: jest.fn(), 9 | taskReloadStart: jest.fn(), 10 | }; 11 | const fixtures = { 12 | 'render with min Props': minProps, 13 | 'render with Props': { 14 | ...minProps, 15 | executionPlan: { 16 | state: 'paused', 17 | cancellable: false, 18 | }, 19 | runningSteps: [ 20 | { 21 | cancellable: false, 22 | id: 1, 23 | action_class: 'test', 24 | state: 'paused', 25 | input: 26 | '{"locale"=>"en",\n "current_request_id"=>nil,\n "current_user_id"=>4,\n "current_organization_id"=>nil,\n "current_location_id"=>nil}\n', 27 | output: '{}\n', 28 | }, 29 | ], 30 | }, 31 | }; 32 | 33 | describe('RunningSteps', () => { 34 | describe('rendering', () => 35 | testComponentSnapshotsWithFixtures(RunningSteps, fixtures)); 36 | }); 37 | -------------------------------------------------------------------------------- /app/services/ui_notifications/tasks/task_paused_owner.rb: -------------------------------------------------------------------------------- 1 | module UINotifications 2 | module Tasks 3 | class TaskPausedOwner < Tasks::Base 4 | def deliver! 5 | notification = ::Notification.new( 6 | :audience => Notification::AUDIENCE_USER, 7 | :notification_blueprint => blueprint, 8 | :initiator => initiator, 9 | :message => message, 10 | :subject => subject 11 | ) 12 | notification.send(:set_custom_attributes) # to add links from blueprint 13 | notification.actions['links'] ||= [] 14 | if troubleshooting_help_generator 15 | notification.actions['links'].concat(troubleshooting_help_generator.links.map { |l| l.to_h(capitalize_title: true) }) 16 | end 17 | notification.save! 18 | notification 19 | end 20 | 21 | def blueprint 22 | @blueprint ||= NotificationBlueprint.unscoped.find_by(:name => 'tasks_paused_owner') 23 | end 24 | 25 | def message 26 | StringParser.new(blueprint.message, subject: subject.action).to_s 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/foreman_tasks/dynflow.rb: -------------------------------------------------------------------------------- 1 | require 'dynflow' 2 | 3 | module ForemanTasks 4 | # Class for configuring and preparing the Dynflow runtime environment. 5 | class Dynflow < ::Dynflow::Rails 6 | require 'foreman_tasks/dynflow/console_authorizer' 7 | 8 | def web_console 9 | ::Dynflow::Web.setup do 10 | before do 11 | if !Setting[:dynflow_enable_console] || 12 | (Setting[:dynflow_console_require_auth] && !ConsoleAuthorizer.from_env(env).allow?) 13 | halt 403, 'Access forbidden' 14 | end 15 | end 16 | 17 | set(:custom_navigation) do 18 | { _('Back to tasks') => "/#{ForemanTasks::TasksController.controller_path}" } 19 | end 20 | set(:world) { ::Rails.application.dynflow.world } 21 | # Do not show Sinatra's pretty error pages in production 22 | set(:show_exceptions) { ::Rails.env.development? } 23 | # Let the errors propagate out from Sinatra to Rails. 24 | # Without this, we'd only get a mostly blank 500 ISE page 25 | set(:raise_errors) { !::Rails.env.development? } 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/Components/__test__/__snapshots__/ActionSelectButton.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ActionSelectButton renders with minimal props 1`] = ` 4 | 9 | 18 | Cancel Selected 19 | 20 | 29 | Resume Selected 30 | 31 | 40 | Force Cancel Selected 41 | 42 | 43 | `; 44 | -------------------------------------------------------------------------------- /app/services/ui_notifications/tasks/task_bulk_stop.rb: -------------------------------------------------------------------------------- 1 | module UINotifications 2 | module Tasks 3 | class TaskBulkStop < ::UINotifications::Base 4 | def initialize(task, stopped_length, skipped_length) 5 | @subject = task 6 | @stopped_length = stopped_length 7 | @skipped_length = skipped_length 8 | end 9 | 10 | def create 11 | Notification.create!( 12 | initiator: initiator, 13 | audience: audience, 14 | subject: subject, 15 | notification_blueprint: blueprint, 16 | message: message, 17 | notification_recipients: [NotificationRecipient.create(:user => User.current)] 18 | ) 19 | end 20 | 21 | def audience 22 | Notification::AUDIENCE_GLOBAL 23 | end 24 | 25 | def message 26 | ('%{stopped} Tasks were stopped. %{skipped} Tasks were already stopped. ' % 27 | { stopped: @stopped_length, 28 | skipped: @skipped_length }) 29 | end 30 | 31 | def blueprint 32 | @blueprint ||= NotificationBlueprint.find_by(name: 'tasks_bulk_stop') 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/views/common/_trigger_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | <% javascript 'foreman_tasks/foreman_tasks' %> 5 | <% stylesheet 'foreman_tasks/foreman_tasks' %> 6 | 7 | <%= javascript_tag do %> 8 | $(function() { trigger_form_selector_binds('<%= f.options[:html][:id] %>','<%= f.object_name %>') }); 9 | <% end %> 10 | 11 | <%= fields_for :triggering, triggering do |trigger_fields| %> 12 | <%= radio_button_f trigger_fields, :mode, :class => 'trigger_mode_selector', :value => 'immediate', :text => _("Execute now") %> 13 | <%= radio_button_f trigger_fields, :mode, :class => 'trigger_mode_selector', :value => 'future', :text => _("Schedule future execution") %> 14 | <%= radio_button_f trigger_fields, :mode, :class => 'trigger_mode_selector', :value => 'recurring', :text => _("Set up recurring execution") %> 15 |
16 |
17 | 18 |
19 | <%= future_mode_fieldset trigger_fields, triggering %> 20 | <%= recurring_mode_fieldset trigger_fields, triggering %> 21 |
22 | <% end %> 23 | 24 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksTimeRow/TasksTimeRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Row } from 'patternfly-react'; 4 | import { noop } from 'foremanReact/common/helpers'; 5 | import { translate as __ } from 'foremanReact/common/I18n'; 6 | 7 | import { timePropType } from '../../TasksDashboardPropTypes'; 8 | import { TASKS_DASHBOARD_AVAILABLE_TIMES } from '../../TasksDashboardConstants'; 9 | import TimeDropDown from './Components/TimeDropDown/TimeDropDown'; 10 | import './TasksTimeRow.scss'; 11 | 12 | const TasksTimeRow = ({ time, updateTime }) => ( 13 | 14 | {__('With focus on last')} 15 | 20 | 21 | ); 22 | 23 | TasksTimeRow.propTypes = { 24 | time: timePropType, 25 | updateTime: PropTypes.func, 26 | }; 27 | 28 | TasksTimeRow.defaultProps = { 29 | time: TASKS_DASHBOARD_AVAILABLE_TIMES.H24, 30 | updateTime: noop, 31 | }; 32 | 33 | export default TasksTimeRow; 34 | -------------------------------------------------------------------------------- /app/services/ui_notifications/tasks/task_bulk_cancel.rb: -------------------------------------------------------------------------------- 1 | module UINotifications 2 | module Tasks 3 | class TaskBulkCancel < ::UINotifications::Base 4 | def initialize(task, cancelled_length, skipped_length) 5 | @subject = task 6 | @cancelled_length = cancelled_length 7 | @skipped_length = skipped_length 8 | end 9 | 10 | def create 11 | Notification.create!( 12 | initiator: initiator, 13 | audience: audience, 14 | subject: subject, 15 | notification_blueprint: blueprint, 16 | message: message, 17 | notification_recipients: [NotificationRecipient.create({ :user => User.current })] 18 | ) 19 | end 20 | 21 | def audience 22 | Notification::AUDIENCE_GLOBAL 23 | end 24 | 25 | def message 26 | ('%{cancelled} Tasks were cancelled. %{skipped} Tasks were skipped. ' % 27 | { cancelled: @cancelled_length, 28 | skipped: @skipped_length }) 29 | end 30 | 31 | def blueprint 32 | @blueprint ||= NotificationBlueprint.find_by(name: 'tasks_bulk_cancel') 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTable.fixtures.js: -------------------------------------------------------------------------------- 1 | import { STATUS } from 'foremanReact/constants'; 2 | 3 | export const minProps = { 4 | getTableItems: jest.fn(), 5 | getBreadcrumbs: jest.fn(), 6 | itemCount: 2, 7 | selectPage: jest.fn(), 8 | selectAllRows: jest.fn(), 9 | unselectAllRows: jest.fn(), 10 | selectRow: jest.fn(), 11 | unselectRow: jest.fn(), 12 | reloadPage: jest.fn(), 13 | selectedRows: [], 14 | perPage: 10, 15 | history: { location: { search: '' } }, 16 | results: ['a', 'b'], 17 | sort: { 18 | by: 'q', 19 | order: 'w', 20 | }, 21 | openClickedModal: jest.fn(), 22 | openModalAction: jest.fn(), 23 | openModal: jest.fn(), 24 | }; 25 | 26 | export default { 27 | 'render with minimal Props': { 28 | ...minProps, 29 | }, 30 | 'render with no results': { 31 | ...minProps, 32 | results: [], 33 | status: STATUS.RESOLVED, 34 | }, 35 | 'render with error Props': { 36 | ...minProps, 37 | results: ['a'], 38 | status: STATUS.ERROR, 39 | }, 40 | 'render with loading Props': { 41 | ...minProps, 42 | results: ['a'], 43 | status: STATUS.PENDING, 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/StoppedTasksCard/StoppedTasksCard.scss: -------------------------------------------------------------------------------- 1 | .stopped-tasks-card { 2 | .stopped-table { 3 | width: 100%; 4 | border: none; 5 | table-layout: fixed; 6 | 7 | thead { 8 | background-color: transparent; 9 | background-image: none; 10 | tr { 11 | th { 12 | font-weight: bold; 13 | border: none; 14 | } 15 | } 16 | } 17 | 18 | td, 19 | th { 20 | text-align: center; 21 | padding-left: 5px; 22 | width: 33%; 23 | 24 | &:first-child { 25 | text-align: left; 26 | } 27 | } 28 | 29 | td { 30 | &.active { 31 | font-weight: bold; 32 | } 33 | } 34 | .data-col { 35 | &:hover { 36 | cursor: pointer; 37 | border: 1px solid #d1d1d1; 38 | background-color: #def3fe; 39 | } 40 | } 41 | } 42 | 43 | .other-active { 44 | text-decoration: underline; 45 | } 46 | 47 | .pficon { 48 | margin-right: 5px; 49 | } 50 | 51 | .btn-link { 52 | font-size: 14px; 53 | padding-top: 0; 54 | padding-bottom: 0; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/SubTasksPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { sprintf, translate as __ } from 'foremanReact/common/I18n'; 4 | import TasksTablePage from './'; 5 | 6 | export const SubTasksPage = props => { 7 | const parentTaskID = props.match.params.id; 8 | const getBreadcrumbs = actionName => ({ 9 | breadcrumbItems: [ 10 | { caption: __('Tasks'), url: `/foreman_tasks/tasks` }, 11 | { 12 | caption: actionName, 13 | url: `/foreman_tasks/tasks/${parentTaskID}`, 14 | }, 15 | { caption: __('Sub tasks') }, 16 | ], 17 | }); 18 | const createHeader = actionName => 19 | actionName ? sprintf(__('Sub tasks of %s'), actionName) : __('Sub tasks'); 20 | return ( 21 | 27 | ); 28 | }; 29 | 30 | SubTasksPage.propTypes = { 31 | match: PropTypes.shape({ 32 | params: PropTypes.object, 33 | }), 34 | }; 35 | 36 | SubTasksPage.defaultProps = { 37 | match: { 38 | params: {}, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/__tests__/TasksTablePage.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | import TasksTablePage from '../TasksTablePage'; 3 | import { minProps } from './TasksTable.fixtures'; 4 | 5 | jest.mock('foremanReact/common/helpers', () => ({ 6 | getURIQuery: () => ({ state: 'stopped' }), 7 | })); 8 | 9 | const history = { 10 | location: { pathname: '/foreman_tasks/tasks', search: '?action="some-name"' }, 11 | }; 12 | const fixtures = { 13 | 'render with minimal props': { ...minProps, history }, 14 | 15 | 'render with Breadcrubs and edit permissions': { 16 | ...minProps, 17 | history, 18 | results: [{ action: 'a', canEdit: true }], 19 | getBreadcrumbs: () => ({ 20 | breadcrumbItems: [ 21 | { caption: 'Tasks', url: `/foreman_tasks/tasks` }, 22 | { 23 | caption: 'action Name', 24 | url: `/foreman_tasks/tasks/someid`, 25 | }, 26 | { caption: 'Sub tasks' }, 27 | ], 28 | }), 29 | }, 30 | }; 31 | 32 | describe('TasksTablePage', () => { 33 | describe('rendering', () => 34 | testComponentSnapshotsWithFixtures(TasksTablePage, fixtures)); 35 | }); 36 | -------------------------------------------------------------------------------- /webpack/index.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: [2, { ignore: [foremanReact/*] }] */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | /* eslint-disable import/extensions */ 4 | import componentRegistry from 'foremanReact/components/componentRegistry'; 5 | import { registerReducer } from 'foremanReact/common/MountingService'; 6 | import reducers from './ForemanTasks/ForemanTasksReducers'; 7 | import ForemanTasks from './ForemanTasks'; 8 | import TasksDashboard from './ForemanTasks/Components/TasksDashboard'; 9 | import TaskDetails from './ForemanTasks/Components/TaskDetails'; 10 | import TasksTable from './ForemanTasks/Components/TasksTable'; 11 | 12 | // register reducers 13 | Object.entries(reducers).forEach(([key, reducer]) => 14 | registerReducer(key, reducer) 15 | ); 16 | 17 | // register components 18 | componentRegistry.register({ 19 | name: 'ForemanTasks', 20 | type: ForemanTasks, 21 | }); 22 | componentRegistry.register({ 23 | name: 'TasksDashboard', 24 | type: TasksDashboard, 25 | }); 26 | componentRegistry.register({ 27 | name: 'TaskDetails', 28 | type: TaskDetails, 29 | }); 30 | componentRegistry.register({ 31 | name: 'TasksTable', 32 | type: TasksTable, 33 | }); 34 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/__tests__/__snapshots__/SubTasksPage.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`SubTasksPage rendering render with minimal props 1`] = ` 4 | 48 | `; 49 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboardHelper.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | getQueryKeyText, 3 | getQueryValueText, 4 | timeToHoursNumber, 5 | } from '../TasksDashboardHelper'; 6 | import { 7 | TASKS_DASHBOARD_AVAILABLE_TIMES, 8 | TASKS_DASHBOARD_QUERY_KEYS_TEXT, 9 | TASKS_DASHBOARD_QUERY_VALUES_TEXT, 10 | } from '../TasksDashboardConstants'; 11 | 12 | const { H12, H24, WEEK } = TASKS_DASHBOARD_AVAILABLE_TIMES; 13 | 14 | describe('TasksDashboard - helpers', () => { 15 | it('should getQueryKeyText', () => { 16 | Object.entries(TASKS_DASHBOARD_QUERY_KEYS_TEXT).forEach(([key, value]) => { 17 | expect(getQueryKeyText(key)).toBe(value); 18 | }); 19 | }); 20 | 21 | it('should getQueryValueText', () => { 22 | Object.entries(TASKS_DASHBOARD_QUERY_VALUES_TEXT).forEach( 23 | ([key, value]) => { 24 | expect(getQueryValueText(key)).toBe(value); 25 | } 26 | ); 27 | }); 28 | 29 | it('should timeToHoursNumber', () => { 30 | expect(timeToHoursNumber(H12)).toBe(12); 31 | expect(timeToHoursNumber(H24)).toBe(24); 32 | expect(timeToHoursNumber(WEEK)).toBe(7 * 24); 33 | 34 | expect(timeToHoursNumber('other')).toBe(24); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /app/services/ui_notifications/tasks/tasks_running_long.rb: -------------------------------------------------------------------------------- 1 | module UINotifications 2 | module Tasks 3 | class TasksRunningLong < Tasks::Base 4 | include Rails.application.routes.url_helpers 5 | 6 | def deliver! 7 | notification = ::Notification.new( 8 | :audience => Notification::AUDIENCE_GLOBAL, 9 | :notification_blueprint => blueprint, 10 | :initiator => initiator, 11 | :message => message, 12 | :subject => nil, 13 | :notification_recipients => [NotificationRecipient.create(:user => User.current)] 14 | ) 15 | notification.actions['links'] ||= [] 16 | notification.actions['links'] << { 17 | href: foreman_tasks_tasks_path(search: subject.query), 18 | title: N_('Long running tasks'), 19 | } 20 | notification.save! 21 | notification 22 | end 23 | 24 | def blueprint 25 | @blueprint ||= NotificationBlueprint.unscoped.find_by(:name => 'tasks_running_long') 26 | end 27 | 28 | def message 29 | _("%{count} tasks are in running or paused state for more than a day") % { count: subject.task_uuids.count } 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/foreman_tasks/test_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'dynflow/testing' 2 | module ForemanTasks 3 | module TestHelpers 4 | def self.use_in_memory_sqlite! 5 | raise 'the in thread world have already been initialized' if @test_in_thread_world 6 | @use_in_memory_sqlite = true 7 | end 8 | 9 | def self.test_in_thread_world 10 | return @test_in_thread_world if @test_in_thread_world 11 | world_config = ForemanTasks.dynflow.config.world_config 12 | if @use_in_memory_sqlite 13 | world_config.persistence_adapter = lambda do |*_args| 14 | ::Dynflow::PersistenceAdapters::Sequel.new('adapter' => 'sqlite', 'database' => ':memory:') 15 | end 16 | end 17 | @test_in_thread_world = ::Dynflow::Testing::InThreadWorld.new(world_config) 18 | end 19 | 20 | module WithInThreadExecutor 21 | extend ActiveSupport::Concern 22 | included do 23 | setup do 24 | @old_dynflow_world = ForemanTasks.dynflow.world 25 | ForemanTasks.dynflow.world = ForemanTasks::TestHelpers.test_in_thread_world 26 | end 27 | 28 | teardown do 29 | ForemanTasks.dynflow.world = @old_dynflow_world 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/TasksTableConstants.js: -------------------------------------------------------------------------------- 1 | import { getControllerSearchProps } from 'foremanReact/constants'; 2 | 3 | export const TASKS_TABLE_ID = 'TASKS_TABLE'; 4 | 5 | export const SELECT_ROWS = 'SELECT_ROWS'; 6 | export const UNSELECT_ROWS = 'UNSELECT_ROWS'; 7 | export const UNSELECT_ALL_ROWS = 'UNSELECT_ALL_ROWS'; 8 | export const SELECT_ALL_ROWS = 'SELECT_ALL_ROWS'; 9 | export const OPEN_SELECT_ALL = 'OPEN_SELECT_ALL'; 10 | 11 | export const BULK_CANCEL_PATH = 'bulk_cancel'; 12 | export const BULK_RESUME_PATH = 'bulk_resume'; 13 | export const BULK_FORCE_CANCEL_PATH = 'bulk_stop'; 14 | 15 | export const CANCEL_MODAL = 'cancelConfirmModal'; 16 | export const RESUME_MODAL = 'resumeConfirmModal'; 17 | export const CANCEL_SELECTED_MODAL = 'cancelSelectedConfirmModal'; 18 | export const RESUME_SELECTED_MODAL = 'resumeSelectedConfirmModal'; 19 | export const FORCE_UNLOCK_MODAL = 'forceUnlockConfirmModal'; 20 | export const FORCE_UNLOCK_SELECTED_MODAL = 'forceUnlockSelectedConfirmModal'; 21 | 22 | export const UPDATE_CLICKED = 'UPDATE_CLICKED'; 23 | export const UPDATE_MODAL = 'UPDATE_MODAL'; 24 | 25 | export const TASKS_SEARCH_PROPS = { 26 | ...getControllerSearchProps('tasks'), 27 | controller: 'foreman_tasks_tasks', 28 | }; 29 | -------------------------------------------------------------------------------- /test/foreman_tasks_test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require_relative './support/dummy_active_job' 3 | require_relative './support/dummy_dynflow_action' 4 | require_relative './support/dummy_recurring_dynflow_action' 5 | require_relative './support/dummy_proxy_action' 6 | require_relative './support/dummy_task_group' 7 | require_relative './support/history_tasks_builder' 8 | 9 | require 'dynflow/testing' 10 | require 'foreman_tasks/test_helpers' 11 | 12 | FactoryBot.definition_file_paths = ["#{ForemanTasks::Engine.root}/test/factories"] 13 | FactoryBot.find_definitions 14 | 15 | ForemanTasks.dynflow.require! 16 | ForemanTasks.dynflow.config.disable_active_record_actions = true 17 | 18 | ForemanTasks::TestHelpers.use_in_memory_sqlite! 19 | 20 | # waits for the passed block to return non-nil value and reiterates it while getting false 21 | # (till some reasonable timeout). Useful for forcing the tests for some event to occur 22 | def wait_for(waiting_message = 'something to happen') 23 | 30.times do 24 | ret = yield 25 | return ret if ret 26 | sleep 0.3 27 | end 28 | raise "waiting for #{waiting_message} was not successful" 29 | end 30 | 31 | def on_postgresql? 32 | ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' 33 | end 34 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/__snapshots__/RunningSteps.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RunningSteps rendering render with Props 1`] = ` 4 |
5 | 11 |

12 | 13 | Action 14 | : 15 | 16 | 17 |

18 |
19 |       test
20 |     
21 |

22 | 23 | State 24 | : 25 | 26 | 27 | paused 28 | 29 |

30 | 31 | Input 32 | : 33 | 34 | 35 |
36 |         {"locale"=>"en",
37 |  "current_request_id"=>nil,
38 |  "current_user_id"=>4,
39 |  "current_organization_id"=>nil,
40 |  "current_location_id"=>nil}
41 | 
42 |       
43 |
44 | 45 | Output 46 | : 47 | 48 | 49 |
50 |         {}
51 | 
52 |       
53 |
54 |
55 |
56 | `; 57 | 58 | exports[`RunningSteps rendering render with min Props 1`] = ` 59 | 60 | No running steps 61 | 62 | `; 63 | -------------------------------------------------------------------------------- /app/models/foreman_tasks/concerns/user_extensions.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | module Concerns 3 | module UserExtensions 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | # rubocop:disable Rails/ReflectionClassName 8 | has_many :tasks, :dependent => :nullify, 9 | :class_name => ::ForemanTasks::Task.name 10 | # rubocop:enable Rails/ReflectionClassName 11 | 12 | before_validation :attach_task_mail_notifications, on: :create 13 | end 14 | 15 | def attach_task_mail_notifications 16 | return if ::ForemanSeeder.is_seeding 17 | 18 | org_admin_role = Role.find_by(name: 'Organization admin') 19 | admin_by_role = org_admin_role && 20 | (roles.map(&:id) & ([org_admin_role.id] + org_admin_role.cloned_role_ids)).any? 21 | 22 | return unless admin || admin_by_role 23 | 24 | notification = MailNotification.find_by(name: 'long_running_tasks') 25 | return if notification.nil? 26 | 27 | if user_mail_notifications.none? { |n| n.mail_notification_id == notification.id } 28 | user_mail_notifications.build(mail_notification_id: notification.id, interval: 'Subscribe') 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/services/ui_notifications/tasks/task_bulk_resume.rb: -------------------------------------------------------------------------------- 1 | module UINotifications 2 | module Tasks 3 | class TaskBulkResume < ::UINotifications::Base 4 | def initialize(task, resumed_length, failed_length, skipped_length) 5 | @subject = task 6 | @resumed_length = resumed_length 7 | @failed_length = failed_length 8 | @skipped_length = skipped_length 9 | end 10 | 11 | def create 12 | Notification.create!( 13 | initiator: initiator, 14 | audience: audience, 15 | subject: subject, 16 | notification_blueprint: blueprint, 17 | message: message, 18 | notification_recipients: [NotificationRecipient.create({ :user => User.current })] 19 | ) 20 | end 21 | 22 | def audience 23 | Notification::AUDIENCE_USER 24 | end 25 | 26 | def message 27 | ('%{resumed} Tasks were resumed. %{failed} Tasks failed to resume. %{skipped} Tasks were skipped. ' % 28 | { resumed: @resumed_length, 29 | failed: @failed_length, 30 | skipped: @skipped_length }) 31 | end 32 | 33 | def blueprint 34 | @blueprint ||= NotificationBlueprint.find_by(name: 'tasks_bulk_resume') 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/__tests__/__snapshots__/TaskDetailsActions.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TaskDetails - Actions should cancelStep 1`] = ` 4 | Array [ 5 | Array [ 6 | Object { 7 | "payload": Object { 8 | "message": Object { 9 | "message": "Trying to cancel step step-id", 10 | "type": "info", 11 | }, 12 | }, 13 | "type": "TOASTS_ADD", 14 | }, 15 | ], 16 | Array [ 17 | Object { 18 | "handleError": [Function], 19 | "handleSuccess": [Function], 20 | "key": "TASK_STEP_CANCEL", 21 | "type": "post-some-type", 22 | "url": "foreman/foreman_tasks/tasks/task-id/cancel_step?step_id=step-id", 23 | }, 24 | ], 25 | ] 26 | `; 27 | 28 | exports[`TaskDetails - Actions should start reload 1`] = ` 29 | Array [ 30 | Array [ 31 | Object { 32 | "handleError": [Function], 33 | "handleSuccess": [Function], 34 | "interval": 5000, 35 | "key": "FOREMAN_TASK_DETAILS", 36 | "type": "get-some-type", 37 | "url": "foreman/foreman_tasks/api/tasks/1/details?include_permissions", 38 | }, 39 | ], 40 | ] 41 | `; 42 | 43 | exports[`TaskDetails - Actions should stop reload 1`] = ` 44 | Object { 45 | "type": "stop", 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/Components/TaskSkeleton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Skeleton from 'react-loading-skeleton'; 3 | import { Grid, Row, Col } from 'patternfly-react'; 4 | 5 | export const TaskSkeleton = () => { 6 | const details = [1, 2, 3, 4, 5, 6]; 7 | return ( 8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 | {details.map((items, key) => ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ))} 31 |
32 | 33 |
34 |
35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foreman-tasks", 3 | "version": "1.0.0", 4 | "description": "Foreman Tasks =============", 5 | "main": "index.js", 6 | "scripts": { 7 | "foreman-js:link": "./script/npm_link_foreman_js.sh", 8 | "lint": "tfm-lint --plugin -d /webpack", 9 | "test": "tfm-test --plugin", 10 | "test:watch": "tfm-test --plugin --watchAll", 11 | "test:current": "tfm-test --plugin --watch", 12 | "publish-coverage": "tfm-publish-coverage" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/theforeman/foreman-tasks.git" 17 | }, 18 | "bugs": { 19 | "url": "http://projects.theforeman.org/projects/foreman-tasks/issues" 20 | }, 21 | "peerDependencies": { 22 | "@theforeman/vendor": ">= 12.1.1" 23 | }, 24 | "dependencies": { 25 | "c3": "^0.4.11" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.7.0", 29 | "@theforeman/builder": ">= 12.1.1", 30 | "@theforeman/eslint-plugin-foreman": ">= 12.1.1", 31 | "@theforeman/test": ">= 12.1.1", 32 | "@theforeman/vendor-dev": ">= 12.1.1", 33 | "babel-eslint": "^10.0.3", 34 | "eslint": "^6.7.2", 35 | "jed": "^1.1.1", 36 | "prettier": "^1.13.5", 37 | "stylelint": "^9.3.0", 38 | "stylelint-config-standard": "^18.0.0", 39 | "surge": "^0.20.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/controllers/recurring_logics_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'foreman_tasks_test_helper' 3 | 4 | module ForemanTasks 5 | class RecurringLogicsControllerTest < ActionController::TestCase 6 | describe ForemanTasks::RecurringLogicsController do 7 | basic_index_test('recurring_logics') 8 | basic_pagination_per_page_test 9 | 10 | # rubocop:disable Naming/AccessorMethodName 11 | def get_factory_name 12 | :recurring_logic 13 | end 14 | # rubocop:enable Naming/AccessorMethodName 15 | 16 | describe 'PUT /recurring_logics/ID/enable' do 17 | let(:recurring_logic) do 18 | recurring_logic = FactoryBot.create(:recurring_logic) 19 | recurring_logic.start(::Support::DummyRecurringDynflowAction) 20 | recurring_logic 21 | end 22 | 23 | it 'disables' do 24 | put :enable, params: { :id => recurring_logic.id }, session: set_session_user 25 | assert_redirected_to '/foreman_tasks/recurring_logics' 26 | end 27 | 28 | it 'enables' do 29 | recurring_logic.update(:enabled => false) 30 | put :disable, params: { :id => recurring_logic.id }, session: set_session_user 31 | 32 | assert_redirected_to '/foreman_tasks/recurring_logics' 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Errors.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import Errors from '../Errors'; 4 | 5 | const fixtures = { 6 | 'render without Props': {}, 7 | 'render with Props': { 8 | executionPlan: { 9 | state: 'paused', 10 | cancellable: false, 11 | }, 12 | failedSteps: [ 13 | { 14 | error: { 15 | exception_class: 'RuntimeError', 16 | message: 17 | 'Action Actions::Katello::EventQueue::Monitor is already active', 18 | backtrace: [ 19 | "/home/vagrant/.gem/ruby/gems/dynflow-1.2.3/lib/dynflow/action/singleton.rb:15:in `rescue in singleton_lock!'", 20 | "/home/vagrant/.gem/ruby/gems/dynflow-1.2.3/lib/dynflow/action/singleton.rb:12:in `singleton_lock!'", 21 | ], 22 | }, 23 | action_class: 'Actions::Katello::EventQueue::Monitor', 24 | state: 'error', 25 | input: 26 | '{"locale"=>"en",\n "current_request_id"=>nil,\n "current_user_id"=>4,\n "current_organization_id"=>nil,\n "current_location_id"=>nil}\n', 27 | output: '{}\n', 28 | }, 29 | ], 30 | }, 31 | }; 32 | 33 | describe('Errors', () => { 34 | describe('rendering', () => 35 | testComponentSnapshotsWithFixtures(Errors, fixtures)); 36 | }); 37 | -------------------------------------------------------------------------------- /test/unit/summarizer_test.rb: -------------------------------------------------------------------------------- 1 | require 'foreman_tasks_test_helper' 2 | 3 | class SummarizerTest < ActiveSupport::TestCase 4 | before do 5 | ::ForemanTasks::Task.delete_all 6 | end 7 | 8 | describe ForemanTasks::Task::Summarizer do 9 | before do 10 | @tasks_builder = HistoryTasksBuilder.new 11 | @tasks_builder.build 12 | end 13 | 14 | let :subject do 15 | ForemanTasks::Task::Summarizer.new(ForemanTasks::Task) 16 | end 17 | 18 | let :expected do 19 | @tasks_builder.distribution 20 | end 21 | 22 | it 'is able to group tasks counts by state and result' do 23 | summary = subject.summary 24 | expected.each do |(state, expected_state_vals)| 25 | assert_summary(expected_state_vals, summary[state], "summary[#{state}]") 26 | expected_state_vals.fetch(:by_result, {}).each do |result, expected_result_vals| 27 | assert_summary expected_result_vals, summary[state][:by_result][result], "summary[#{state}][#{result}]" 28 | end 29 | end 30 | end 31 | 32 | def assert_summary(expected_summary, summary, value_desc) 33 | %I[recent total].each do |key| 34 | assert_equal expected_summary[key], summary[key], 35 | "#{value_desc}[#{key}] expected to be #{expected_summary[key]}, was #{summary[key]}" 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/lib/concerns/polling_action_extensions_test.rb: -------------------------------------------------------------------------------- 1 | require 'foreman_tasks_test_helper' 2 | 3 | module ForemanTasks 4 | module Concerns 5 | class PollingActionExtensionsTest < ::ActiveSupport::TestCase 6 | class Action < ::Dynflow::Action 7 | include ::Dynflow::Action::Polling 8 | end 9 | 10 | describe 'polling interval tuning' do 11 | let(:default_intervals) { [0.5, 1, 2, 4, 8, 16] } 12 | 13 | it 'is extends the polling action module' do 14 | assert_equal ForemanTasks::Concerns::PollingActionExtensions, ::Dynflow::Action::Polling.ancestors.first 15 | end 16 | 17 | it 'does not modify polling intervals by default' do 18 | assert_equal default_intervals, Action.allocate.poll_intervals 19 | end 20 | 21 | it 'cannot make intervals shorter than 0.5 seconds' do 22 | Setting.expects(:[]).with(:foreman_tasks_polling_multiplier).returns 0 23 | assert_equal default_intervals.map { 0.5 }, Action.allocate.poll_intervals 24 | end 25 | 26 | it 'can be used to make the intervals longer' do 27 | value = 5 28 | Setting.expects(:[]).with(:foreman_tasks_polling_multiplier).returns value 29 | assert_equal default_intervals.map { |i| i * value }, Action.allocate.poll_intervals 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/__tests__/TasksDashboardActions.test.js: -------------------------------------------------------------------------------- 1 | import { testActionSnapshotWithFixtures } from '@theforeman/test'; 2 | import { API } from 'foremanReact/redux/API'; 3 | import { timeToHoursNumber } from '../TasksDashboardHelper'; 4 | import { 5 | initializeDashboard, 6 | updateTime, 7 | updateQuery, 8 | fetchTasksSummary, 9 | } from '../TasksDashboardActions'; 10 | import { 11 | correctTime, 12 | wrongTime, 13 | parentTaskID, 14 | apiGetMock, 15 | } from './TaskDashboard.fixtures'; 16 | 17 | jest.mock('foremanReact/redux/API'); 18 | jest.mock('../TasksDashboardHelper'); 19 | 20 | timeToHoursNumber.mockImplementation(arg => arg); 21 | API.get.mockImplementation(apiGetMock); 22 | 23 | const fixtures = { 24 | 'should initialize-dashboard': () => 25 | initializeDashboard({ time: 'some-time', query: 'some-query' }), 26 | 'should update-time': () => updateTime('some-time'), 27 | 'should update-query': () => updateQuery('some-query'), 28 | 'should fetch-tasks-summary and success': () => 29 | fetchTasksSummary(correctTime), 30 | 'should fetch-tasks-summary for subtasks and success': () => 31 | fetchTasksSummary(correctTime, parentTaskID), 32 | 'should fetch-tasks-summary and fail': () => fetchTasksSummary(wrongTime), 33 | }; 34 | 35 | describe('TasksDashboard - Actions', () => 36 | testActionSnapshotWithFixtures(fixtures)); 37 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/__tests__/__snapshots__/TasksDashboard.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TasksDashboard rendering render without Props 1`] = ` 4 | 10 | 14 | 49 | 53 | 54 | `; 55 | -------------------------------------------------------------------------------- /db/migrate/20180216092715_use_uuid.rb: -------------------------------------------------------------------------------- 1 | class UseUuid < ActiveRecord::Migration[5.0] 2 | # PostgreSQL has a special column type for storing UUIDs. 3 | # Using this type instead of generic string should lead to having 4 | # smaller DB and possibly better overall performance. 5 | def up 6 | if on_postgresql? 7 | change_table :foreman_tasks_tasks do |t| 8 | t.change :id, :uuid, :using => 'id::uuid' 9 | t.change :parent_task_id, :uuid, :using => 'parent_task_id::uuid' 10 | end 11 | 12 | change_table :foreman_tasks_task_group_members do |t| 13 | t.change :task_id, :uuid, :using => 'task_id::uuid' 14 | end 15 | 16 | change_table :foreman_tasks_locks do |t| 17 | t.change :task_id, :uuid, :using => 'task_id::uuid' 18 | end 19 | end 20 | end 21 | 22 | def down 23 | if on_postgresql? 24 | change_table :foreman_tasks_tasks do |t| 25 | t.change :id, :string 26 | t.change :parent_task_id, :string 27 | end 28 | 29 | change_table :foreman_tasks_task_group_members do |t| 30 | t.change :task_id, :string 31 | end 32 | 33 | change_table :foreman_tasks_locks do |t| 34 | t.change :task_id, :string 35 | end 36 | end 37 | end 38 | 39 | private 40 | 41 | def on_postgresql? 42 | ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/foreman_tasks/dynflow/configuration.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('lib/foreman/dynflow/configuration', ::Rails.root) 2 | 3 | module ForemanTasks 4 | # Import all Dynflow configuration from Foreman, and add our own for Tasks 5 | class Dynflow::Configuration < ::Foreman::Dynflow::Configuration 6 | def world_config 7 | super.tap do |config| 8 | config.backup_deleted_plans = backup_settings[:backup_deleted_plans] 9 | config.backup_dir = backup_settings[:backup_dir] 10 | end 11 | end 12 | 13 | def backup_settings 14 | return @backup_settings if @backup_settings 15 | backup_options = { 16 | :backup_deleted_plans => true, 17 | :backup_dir => default_backup_dir, 18 | } 19 | settings = SETTINGS.dig(:'foreman-tasks', :backup) 20 | backup_options.merge!(settings) if settings 21 | @backup_settings = with_environment_override backup_options 22 | end 23 | 24 | def default_backup_dir 25 | File.join(Rails.root, 'tmp', 'task-backup') 26 | end 27 | 28 | def with_environment_override(options) 29 | env_var = ENV['TASK_BACKUP'] 30 | unless env_var.nil? 31 | # Everything except 0, n, no, false is considered to be a truthy value 32 | options[:backup_deleted_plans] = !%w[0 n no false].include?(env_var.downcase) 33 | end 34 | options 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/tasks/generate_task_actions_test.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'foreman_tasks_test_helper' 3 | 4 | class GenerateTaskActionsTest < ActiveSupport::TestCase 5 | TASK_NAME = 'foreman_tasks:generate_task_actions'.freeze 6 | 7 | setup do 8 | Rake.application.rake_require 'foreman_tasks/tasks/generate_task_actions' 9 | 10 | Rake::Task.define_task(:environment) 11 | Rake::Task[TASK_NAME].reenable 12 | end 13 | 14 | let(:tasks) do 15 | (1..5).map { FactoryBot.build(:dynflow_task) } 16 | end 17 | 18 | it 'fixes the tasks' do 19 | label = 'a label' 20 | tasks 21 | ForemanTasks::Task.update_all(:action => nil) 22 | ForemanTasks::Task.any_instance.stubs(:to_label).returns(label) 23 | 24 | stdout, _stderr = capture_io do 25 | Rake.application.invoke_task TASK_NAME 26 | end 27 | 28 | assert_match(%r{Generating action for #{tasks.count} tasks}, stdout) 29 | assert_equal tasks.count, ForemanTasks::Task.where(:action => label).count 30 | assert_match(%r{Processed #{tasks.count}/#{tasks.count} tasks}, stdout) 31 | end 32 | 33 | it 'fixes only tasks with missing action' do 34 | tasks 35 | ForemanTasks::Task.any_instance.expects(:save!).never 36 | 37 | stdout, _stderr = capture_io do 38 | Rake.application.invoke_task TASK_NAME 39 | end 40 | 41 | assert_match(%r{Generating action for 0 tasks}, stdout) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/Components/ActionSelectButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DropdownButton, MenuItem } from 'patternfly-react'; 3 | import PropTypes from 'prop-types'; 4 | import { translate as __ } from 'foremanReact/common/I18n'; 5 | 6 | export const ActionSelectButton = ({ 7 | onCancel, 8 | onResume, 9 | onForceCancel, 10 | disabled, 11 | }) => ( 12 | 17 | 22 | {__('Cancel Selected')} 23 | 24 | 29 | {__('Resume Selected')} 30 | 31 | 36 | {__('Force Cancel Selected')} 37 | 38 | 39 | ); 40 | 41 | ActionSelectButton.propTypes = { 42 | disabled: PropTypes.bool, 43 | onCancel: PropTypes.func.isRequired, 44 | onResume: PropTypes.func.isRequired, 45 | onForceCancel: PropTypes.func.isRequired, 46 | }; 47 | 48 | ActionSelectButton.defaultProps = { 49 | disabled: false, 50 | }; 51 | -------------------------------------------------------------------------------- /app/lib/actions/middleware/keep_current_timezone.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | module Middleware 3 | class KeepCurrentTimezone < Dynflow::Middleware 4 | def delay(*args) 5 | pass(*args).tap { store_current_timezone } 6 | end 7 | 8 | def plan(*args) 9 | with_current_timezone do 10 | pass(*args).tap { store_current_timezone } 11 | end 12 | end 13 | 14 | def run(*args) 15 | restore_curent_timezone { pass(*args) } 16 | end 17 | 18 | def finalize 19 | restore_curent_timezone { pass } 20 | end 21 | 22 | # Run all execution plan lifecycle hooks as the original timezone 23 | def hook(*args) 24 | restore_curent_timezone { pass(*args) } 25 | end 26 | 27 | private 28 | 29 | def with_current_timezone 30 | if action.input[:current_timezone].nil? 31 | yield 32 | else 33 | restore_curent_timezone { yield } 34 | end 35 | end 36 | 37 | def store_current_timezone 38 | action.input[:current_timezone] = Time.zone.name 39 | end 40 | 41 | def restore_curent_timezone 42 | old_zone = Time.zone 43 | Time.zone = Time.find_zone(action.input[:current_timezone]) if action.input[:current_timezone].present? 44 | yield 45 | ensure 46 | Time.zone = old_zone 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/lib/actions/middleware/proxy_batch_triggering.rb: -------------------------------------------------------------------------------- 1 | module Actions 2 | module Middleware 3 | class ProxyBatchTriggering < ::Dynflow::Middleware 4 | # If the event could result into sub tasks being planned, check if there are any RemoteTasks 5 | # to trigger after the event is processed 6 | # 7 | # The ProxyAction needs to be planned with `:use_batch_triggering => true` to activate the feature 8 | def run(event = nil) 9 | pass event 10 | ensure 11 | trigger_remote_tasks if event.nil? || event.is_a?(Dynflow::Action::WithBulkSubPlans::PlanNextBatch) 12 | end 13 | 14 | def trigger_remote_tasks 15 | # Find the tasks in batches, order them by proxy_url so we get all tasks 16 | # to a certain proxy "close to each other" 17 | remote_tasks.pending.order(:proxy_url, :id).find_in_batches(:batch_size => batch_size) do |batch| 18 | # Group the tasks by operation, in theory there should be only one operation 19 | batch.group_by(&:operation).each do |operation, group| 20 | ForemanTasks::RemoteTask.batch_trigger(operation, group) 21 | end 22 | end 23 | end 24 | 25 | def remote_tasks 26 | action.task.remote_sub_tasks 27 | end 28 | 29 | private 30 | 31 | def batch_size 32 | Setting['foreman_tasks_proxy_batch_size'] 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/foreman_tasks/tasks/generate_task_actions.rake: -------------------------------------------------------------------------------- 1 | # 2 | # generate_task_actions.rake is a data migration task to properly fill the missing 3 | # values into the foreman_tasks_tasks table. 4 | # 5 | # Run "foreman-rake foreman_tasks:generate_task_actions" to generate missing values. 6 | 7 | namespace :foreman_tasks do 8 | desc 'Generate missing values for action column in foreman_tasks_tasks table.' 9 | 10 | BATCH_SIZE = 100 11 | 12 | task :generate_task_actions => :environment do 13 | class ProgressReporter 14 | def initialize(count, message = nil) 15 | @count = count 16 | @processed = 0 17 | puts message % { :count => count } unless message.nil? 18 | end 19 | 20 | def progress(count) 21 | @processed += count 22 | end 23 | 24 | def report 25 | puts _('Processed %{processed}/%{count} tasks') % { :processed => @processed, :count => @count } 26 | end 27 | end 28 | 29 | scope = ::ForemanTasks::Task.where(:action => nil).order(:started_at => :desc) 30 | count = scope.count 31 | reporter = ProgressReporter.new count, _('Generating action for %{count} tasks.') 32 | scope.find_in_batches(:batch_size => BATCH_SIZE) do |group| 33 | group.each do |task| 34 | task.action = task.to_label 35 | task.save! 36 | end 37 | reporter.progress group.size 38 | reporter.report 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /db/seeds.d/30-notification_blueprints.rb: -------------------------------------------------------------------------------- 1 | blueprints = [ 2 | { 3 | group: N_('Tasks'), 4 | name: 'tasks_paused_admin', 5 | message: "DYNAMIC", 6 | level: 'warning', 7 | actions: 8 | { 9 | links: 10 | [ 11 | href: "/foreman_tasks/tasks?search=#{CGI.escape('state=paused')}", 12 | title: N_('List of tasks'), 13 | ], 14 | }, 15 | }, 16 | 17 | { 18 | group: N_('Tasks'), 19 | name: 'tasks_paused_owner', 20 | message: "The task '%{subject}' got paused", 21 | level: 'warning', 22 | actions: 23 | { 24 | links: 25 | [ 26 | path_method: :foreman_tasks_task_path, 27 | title: N_('Task Details'), 28 | ], 29 | }, 30 | }, 31 | 32 | { 33 | group: N_('Tasks'), 34 | name: 'tasks_bulk_resume', 35 | level: 'info', 36 | message: "DYNAMIC", 37 | }, 38 | 39 | { 40 | group: N_('Tasks'), 41 | name: 'tasks_bulk_cancel', 42 | level: 'info', 43 | message: "DYNAMIC", 44 | }, 45 | 46 | { 47 | group: N_('Tasks'), 48 | name: 'tasks_bulk_stop', 49 | level: 'info', 50 | message: "DYNAMIC", 51 | }, 52 | 53 | { 54 | group: N_('Tasks'), 55 | name: 'tasks_running_long', 56 | message: 'DYNAMIC', 57 | level: 'warning', 58 | }, 59 | ] 60 | 61 | blueprints.each { |blueprint| UINotifications::Seed.new(blueprint).configure } 62 | -------------------------------------------------------------------------------- /app/lib/actions/recurring_action.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | module Actions 4 | module RecurringAction 5 | # When included sets the base action to use the RecurringLogic middleware and configures 6 | # #trigger_repeat to be called when appropriate to trigger the next repeat. 7 | def self.included(base) 8 | base.middleware.use Actions::Middleware::RecurringLogic 9 | base.execution_plan_hooks.use :trigger_repeat, :on => [:planned, :failure] 10 | end 11 | 12 | # Hook to be called when a repetition needs to be triggered. This either happens when the plan goes into planned state 13 | # or when it fails. 14 | def trigger_repeat(execution_plan) 15 | request_id = ::Logging.mdc['request'] 16 | ::Logging.mdc['request'] = SecureRandom.uuid 17 | if execution_plan.delay_record && recurring_logic_task_group 18 | args = execution_plan.delay_record.args 19 | logic = recurring_logic_task_group.recurring_logic 20 | task_start_at = [task.start_at, Time.zone.now].max 21 | logic.trigger_repeat_after(task_start_at, self.class, *args) 22 | end 23 | ensure 24 | ::Logging.mdc['request'] = request_id 25 | end 26 | 27 | private 28 | 29 | def recurring_logic_task_group 30 | @task_group ||= task.task_groups 31 | .find { |tg| tg.is_a? ::ForemanTasks::TaskGroups::RecurringLogicTaskGroup } 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/models/foreman_tasks/concerns/action_subject.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | module Concerns 3 | module ActionSubject 4 | extend ActiveSupport::Concern 5 | 6 | def action_input_key 7 | self.class.name.demodulize.underscore 8 | end 9 | 10 | def to_action_input 11 | raise 'The resource needs to be saved first' if new_record? 12 | 13 | { id: id, name: name }.tap do |hash| 14 | hash.update(label: label) if respond_to? :label 15 | end 16 | end 17 | 18 | # @api override to return the objects that relate to this one, usually parent 19 | # objects, e.g. repository would return product it belongs to, product would return 20 | # provider etc. 21 | # 22 | # It's used to link a task running on top of this resource to it's related objects, 23 | # so that is't possible to see all the sync tasks for a product etc. 24 | def related_resources 25 | [] 26 | end 27 | 28 | # Recursively searches for related resources of this one, avoiding cycles 29 | def all_related_resources 30 | mine = Set.new Array(related_resources) 31 | 32 | get_all_related_resources = lambda do |resource| 33 | resource.is_a?(ActionSubject) ? resource.all_related_resources : [] 34 | end 35 | 36 | mine + mine.reduce(Set.new) { |s, resource| s + get_all_related_resources.call(resource) } 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss: -------------------------------------------------------------------------------- 1 | .task-details-react { 2 | .progress-label-top-right { 3 | font-size: 11px; 4 | text-align: right; 5 | } 6 | a, 7 | button { 8 | margin-right: 3px; 9 | } 10 | .container { 11 | margin: 0; 12 | } 13 | .spin { 14 | -webkit-animation: spin 1s infinite linear; 15 | -moz-animation: spin 1s infinite linear; 16 | -o-animation: spin 1s infinite linear; 17 | animation: spin 1s infinite linear; 18 | -webkit-transform-origin: 50% 50%; 19 | transform-origin: 50% 50%; 20 | -ms-transform-origin: 50% 50%; /* IE 9 */ 21 | } 22 | 23 | @-moz-keyframes spin { 24 | from { 25 | -moz-transform: rotate(0deg); 26 | } 27 | to { 28 | -moz-transform: rotate(360deg); 29 | } 30 | } 31 | 32 | @-webkit-keyframes spin { 33 | from { 34 | -webkit-transform: rotate(0deg); 35 | } 36 | to { 37 | -webkit-transform: rotate(360deg); 38 | } 39 | } 40 | 41 | @keyframes spin { 42 | from { 43 | transform: rotate(0deg); 44 | } 45 | to { 46 | transform: rotate(360deg); 47 | } 48 | } 49 | .dynflow-button > span { 50 | pointer-events: auto; 51 | } 52 | 53 | pre { 54 | white-space: pre-wrap; 55 | word-break: normal; 56 | } 57 | 58 | .param-name { 59 | display: inline-block; 60 | width: 10em; 61 | } 62 | 63 | .details-name { 64 | overflow-wrap: anywhere; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksTable/index.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { bindActionCreators } from 'redux'; 3 | import TasksTablePage from './TasksTablePage'; 4 | import reducer from './TasksTableReducer'; 5 | import * as tableActions from './TasksTableActions'; 6 | import * as bulkActions from './TasksBulkActions'; 7 | import { 8 | selectStatus, 9 | selectError, 10 | selectResults, 11 | selectPerPage, 12 | selectItemCount, 13 | selectSort, 14 | selectActionName, 15 | selectSelectedRows, 16 | selectAllRowsSelected, 17 | selectShowSelectAll, 18 | selectModalID, 19 | selectPermissions, 20 | } from './TasksTableSelectors'; 21 | 22 | const mapStateToProps = state => ({ 23 | status: selectStatus(state), 24 | error: selectError(state), 25 | sort: selectSort(state), 26 | results: selectResults(state), 27 | perPage: selectPerPage(state), 28 | itemCount: selectItemCount(state), 29 | actionName: selectActionName(state), 30 | selectedRows: selectSelectedRows(state), 31 | allRowsSelected: selectAllRowsSelected(state), 32 | showSelectAll: selectShowSelectAll(state), 33 | modalID: selectModalID(state), 34 | permissions: selectPermissions(state), 35 | }); 36 | 37 | const mapDispatchToProps = dispatch => 38 | bindActionCreators({ ...tableActions, ...bulkActions }, dispatch); 39 | 40 | export const reducers = { tasksTable: reducer }; 41 | 42 | export default connect(mapStateToProps, mapDispatchToProps)(TasksTablePage); 43 | -------------------------------------------------------------------------------- /lib/tasks/gettext.rake: -------------------------------------------------------------------------------- 1 | gettext_find_task = begin 2 | Rake::Task['gettext:find'] 3 | rescue 4 | nil 5 | end 6 | 7 | if gettext_find_task 8 | namespace :gettext do 9 | task :store_action_names => :environment do 10 | storage_file = "#{locale_path}/action_names.rb" 11 | method_names = [:plan, :run, :finalize] 12 | instances = Actions::EntryAction 13 | .descendants 14 | .uniq 15 | .map(&:allocate) 16 | .select do |action| 17 | method_names.any? do |method_name| 18 | if action.respond_to?(method_name) 19 | src, = action.method(method_name).source_location 20 | src.start_with? @engine.root.to_s 21 | end 22 | end 23 | end 24 | 25 | if instances.any? 26 | puts "writing action translations to: #{storage_file}" 27 | 28 | File.write storage_file, 29 | "# Autogenerated!\n" + 30 | instances 31 | .map { |instance| %[_("#{instance.humanized_name}")] } 32 | .sort 33 | .join("\n") + "\n" 34 | elsif File.exist? storage_file 35 | puts "Removing empty action translations file: #{storage_file}" 36 | File.delete storage_file 37 | end 38 | end 39 | end 40 | 41 | gettext_find_task.enhance ['gettext:store_action_names'] 42 | end 43 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/TasksDonutChart/TasksDonutChart.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import { TASKS_DONUT_CHART_FOCUSED_ON_OPTIONS_ARRAY } from './TasksDonutChartConstants'; 4 | import TasksDonutChart from './TasksDonutChart'; 5 | 6 | jest.mock('./TasksDonutChartHelper', () => ({ 7 | shouleBeSelected: focusedOn => focusedOn !== 'normal' && focusedOn !== 'none', 8 | getBaseChartConfig: jest.fn(() => ({ base: 'some-base-config' })), 9 | createChartData: jest.fn(() => ({ 10 | columns: 'some-columns', 11 | names: 'some-names', 12 | onItemClick: jest.fn(), 13 | })), 14 | updateChartTitle: jest.fn(), 15 | })); 16 | 17 | const createRequiredProps = () => ({ last: 3, older: 5 }); 18 | 19 | const fixtures = { 20 | 'render with minimal props': { ...createRequiredProps() }, 21 | 'render with props': { 22 | ...createRequiredProps(), 23 | className: 'some-class', 24 | time: 'time-period', 25 | colorsPattern: ['color1', 'color2'], 26 | onTotalClick: jest.fn(), 27 | onLastClick: jest.fn(), 28 | onOlderClick: jest.fn(), 29 | }, 30 | }; 31 | 32 | TASKS_DONUT_CHART_FOCUSED_ON_OPTIONS_ARRAY.forEach(mode => { 33 | fixtures[`render with focused-on ${mode}`] = { 34 | ...createRequiredProps(), 35 | focusedOn: mode, 36 | }; 37 | }); 38 | 39 | describe('TasksDonutChart', () => 40 | testComponentSnapshotsWithFixtures(TasksDonutChart, fixtures)); 41 | -------------------------------------------------------------------------------- /db/migrate/20200517215015_rename_bookmarks_controller.rb: -------------------------------------------------------------------------------- 1 | class RenameBookmarksController < ActiveRecord::Migration[5.2] 2 | def up 3 | original_controller = 'foreman_tasks_tasks' 4 | original_bookmarks = Bookmark.where(controller: original_controller) 5 | original_bookmarks_names = Hash[original_bookmarks.pluck(:name, :id)] 6 | 7 | new_controller = 'foreman_tasks/tasks' 8 | new_bookmarks = Bookmark.where(controller: new_controller) 9 | new_bookmarks.find_each do |new_bookmark| 10 | name = new_bookmark.name 11 | is_name_taken = original_bookmarks_names.key? name 12 | 13 | if is_name_taken 14 | original_bookmark = original_bookmarks.find(original_bookmarks_names[name]) 15 | is_duplicated = original_bookmark.query == new_bookmark.query && 16 | original_bookmark.owner_id == new_bookmark.owner_id && 17 | original_bookmark.owner_type == new_bookmark.owner_type && 18 | original_bookmark.public == new_bookmark.public 19 | 20 | if is_duplicated 21 | original_bookmark.destroy 22 | else 23 | modified_name = "#{name}_#{generate_token}" 24 | original_bookmark.update(name: modified_name) 25 | end 26 | end 27 | # Revert to the original controller name 28 | new_bookmark.update(controller: original_controller) 29 | end 30 | end 31 | 32 | def generate_token 33 | SecureRandom.base64(5).gsub(/[^0-9a-z ]/i, '') 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/helpers/foreman_tasks/foreman_tasks_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'foreman_tasks_test_helper' 2 | 3 | module ForemanTasks 4 | class ForemanTasksHelperTest < ActionView::TestCase 5 | describe 'breadcrumb items' do 6 | before do 7 | self.class.send(:include, ForemanTasks::TasksHelper) 8 | end 9 | 10 | it 'prepares items for index correctly' do 11 | stubs(:action_name).returns('index') 12 | items = breadcrumb_items 13 | assert_equal 1, items.count 14 | assert_equal 'Tasks', items.first[:caption] 15 | assert_nil items.first[:url] 16 | end 17 | 18 | it 'prepares items for show correctly' do 19 | @task = FactoryBot.build(:dynflow_task, :user_create_task) 20 | @task.action = 'A task' 21 | stubs(:action_name).returns('show') 22 | items = breadcrumb_items 23 | assert_equal(['Tasks', 'A task'], items.map { |i| i[:caption] }) 24 | assert_nil items.last[:url] 25 | end 26 | 27 | it 'prepares items for sub tasks correctly' do 28 | @task = FactoryBot.build(:dynflow_task, :user_create_task) 29 | child = FactoryBot.build(:dynflow_task, :user_create_task) 30 | @task.sub_tasks = [child] 31 | @task.action = 'A task' 32 | stubs(:action_name).returns('sub_tasks') 33 | items = breadcrumb_items 34 | assert_equal(['Tasks', 'A task', 'Sub tasks'], items.map { |i| i[:caption] }) 35 | assert_nil items.last[:url] 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/foreman_tasks/continuous_output.rb: -------------------------------------------------------------------------------- 1 | module ForemanTasks 2 | class ContinuousOutput 3 | attr_accessor :raw_outputs 4 | 5 | def initialize(raw_outputs = []) 6 | @raw_outputs = [] 7 | raw_outputs.each { |raw_output| add_raw_output(raw_output) } 8 | end 9 | 10 | def add_raw_output(raw_output) 11 | missing_args = %w[output_type output timestamp] - raw_output.keys 12 | unless missing_args.empty? 13 | raise ArgumentError, "Missing args for raw output: #{missing_args.inspect}" 14 | end 15 | @raw_outputs << raw_output 16 | end 17 | 18 | def empty? 19 | @raw_outputs.empty? 20 | end 21 | 22 | def last_timestamp 23 | return if @raw_outputs.empty? 24 | @raw_outputs.last.fetch('timestamp') 25 | end 26 | 27 | def sort! 28 | @raw_outputs.sort_by! { |record| record['timestamp'].to_f } 29 | end 30 | 31 | def humanize 32 | sort! 33 | raw_outputs.map { |output| output['output'] }.join("\n") 34 | end 35 | 36 | def add_exception(context, exception, timestamp = Time.now.getlocal) 37 | add_output(context + ": #{exception.class} - #{exception.message}", 'debug', timestamp) 38 | end 39 | 40 | def add_output(*args) 41 | add_raw_output(self.class.format_output(*args)) 42 | end 43 | 44 | def self.format_output(message, type = 'debug', timestamp = Time.now.getlocal) 45 | { 'output_type' => type, 46 | 'output' => message, 47 | 'timestamp' => timestamp.to_f } 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /webpack/ForemanTasks/Components/TasksDashboard/Components/TasksCardsGrid/Components/TasksDonutCard/TasksDonutCard.test.js: -------------------------------------------------------------------------------- 1 | import { testComponentSnapshotsWithFixtures } from '@theforeman/test'; 2 | 3 | import { 4 | TASKS_DASHBOARD_AVAILABLE_TIMES, 5 | TASKS_DASHBOARD_AVAILABLE_QUERY_MODES, 6 | } from '../../../../TasksDashboardConstants'; 7 | import TasksDonutCard from './TasksDonutCard'; 8 | 9 | const fixtures = { 10 | 'render with minimal props': {}, 11 | 'render with props': { 12 | title: 'some title', 13 | time: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK, 14 | wantedQueryState: 'some-state', 15 | className: 'some-classname', 16 | data: { last: 3, older: 5 }, 17 | }, 18 | 'render with total selected': { 19 | wantedQueryState: 'some-state', 20 | query: { state: 'some-state' }, 21 | }, 22 | 'render with last selected': { 23 | wantedQueryState: 'some-state', 24 | time: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK, 25 | query: { 26 | state: 'some-state', 27 | mode: TASKS_DASHBOARD_AVAILABLE_QUERY_MODES.LAST, 28 | time: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK, 29 | }, 30 | }, 31 | 'render with older selected': { 32 | wantedQueryState: 'some-state', 33 | time: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK, 34 | query: { 35 | state: 'some-state', 36 | mode: TASKS_DASHBOARD_AVAILABLE_QUERY_MODES.OLDER, 37 | time: TASKS_DASHBOARD_AVAILABLE_TIMES.WEEK, 38 | }, 39 | }, 40 | }; 41 | 42 | describe('TasksDonutCard', () => 43 | testComponentSnapshotsWithFixtures(TasksDonutCard, fixtures)); 44 | --------------------------------------------------------------------------------