foreman-tasks 12.2.4 → 13.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/foreman_tasks/tasks_controller.rb +0 -5
  3. data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
  4. data/config/routes.rb +3 -2
  5. data/lib/foreman_tasks/engine.rb +2 -2
  6. data/lib/foreman_tasks/version.rb +1 -1
  7. data/test/controllers/tasks_controller_test.rb +0 -9
  8. data/test/foreman_tasks_test_helper.rb +2 -2
  9. data/test/integration/tasks_test.rb +17 -0
  10. data/test/test_plugin_helper.rb +8 -0
  11. data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +8 -70
  12. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +115 -104
  13. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.scss +45 -0
  14. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js +54 -0
  15. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +237 -177
  16. data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskSkeleton.js +1 -6
  17. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -35
  18. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -69
  19. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js +118 -1
  20. data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskInfo.test.js +190 -20
  21. data/webpack/ForemanTasks/Components/TaskDetails/ExecutionDetails.js +65 -0
  22. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +76 -65
  23. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +1 -44
  24. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsConstants.js +2 -1
  25. data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +20 -10
  26. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +194 -0
  27. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +100 -1
  28. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +226 -18
  29. data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetailsSelectors.test.js +81 -0
  30. data/webpack/ForemanTasks/Components/common/taskResultIcon.js +53 -0
  31. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.js +174 -0
  32. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.scss +5 -0
  33. data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +51 -0
  34. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
  35. data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +277 -0
  36. data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +13 -12
  37. data/webpack/Routes/routes.js +6 -0
  38. data/webpack/Routes/routes.test.js +16 -5
  39. data/webpack/index.js +0 -5
  40. metadata +16 -3
  41. data/app/views/foreman_tasks/tasks/show.html.erb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23f5d844abe59e29a92b15f62c0770e4c3c1b577a1315232c422d976fa77a07e
4
- data.tar.gz: 5d6316649f6cb75894b966301e5d8abefa579d472ddb33052d06e9d32210b869
3
+ metadata.gz: 270e537ec64af9327b2598da48e8422a56adc1716e45ee5105cb57c9bd2d1078
4
+ data.tar.gz: da3627e82bdecfcd63516cf3e973461b22762023374ee58ac105e7f9e66c2cf7
5
5
  SHA512:
6
- metadata.gz: 7b16c2a1cd40296f38805bddb2a96879ed09d4288e7dc03c8061ba6b561333c96f9e9aa213b583349bce844580b85cc24833ca49f617e67730d3e6b28edf4773
7
- data.tar.gz: 2b147d1a8a95389cb7c6a12c4fefea577cdec04f8c0ca66315b4411e62f968b1255b4a82438c014300bfd8d67eb0b9a0bea6fcaf277f1eb2d7058a8feaf5375f
6
+ metadata.gz: dc37e5ebbdb2aa62a131d2828a619f00c68b1c6c262a78df83671801b7047eeafd2bd31fd43e52fff79d7a9b2bb69267a950bd2e71babc6569c20d55a90caab2
7
+ data.tar.gz: ba180fe7ad221035c14474ed9b1cc62795d2d90ff585fdc0304bda52432e43917829d016abd03e18e68ffcb9654f1a73864695d93fe1219214f7a14d43de0033
@@ -6,11 +6,6 @@ module ForemanTasks
6
6
 
7
7
  before_action :find_dynflow_task, only: [:unlock, :force_unlock, :cancel, :abort, :cancel_step, :resume]
8
8
 
9
- def show
10
- @task = resource_base.find(params[:id])
11
- render :layout => !request.xhr?
12
- end
13
-
14
9
  def index
15
10
  params[:order] ||= 'started_at DESC'
16
11
  respond_with_tasks resource_base
@@ -67,7 +67,7 @@ module ForemanTasks
67
67
  i.add_line _('A paused task represents a process that has not finished properly. '\
68
68
  'Any task in paused state can lead to potential inconsistency '\
69
69
  'and needs to be resolved.')
70
- i.add_line _("The recommended approach is to investigate the error messages below and in 'errors' tab, "\
70
+ i.add_line _("The recommended approach is to investigate the error messages below and in 'Execution details' tab, "\
71
71
  'address the primary cause of the issue and resume the task.')
72
72
  if (link = troubleshooting_link)
73
73
  i.add_link(link)
data/config/routes.rb CHANGED
@@ -12,7 +12,7 @@ Foreman::Application.routes.draw do
12
12
  end
13
13
  end
14
14
 
15
- resources :tasks, :only => [:show] do
15
+ resources :tasks, :only => [] do
16
16
  collection do
17
17
  get 'auto_complete_search'
18
18
  get '/summary/:recent_timeframe', action: 'summary'
@@ -27,7 +27,7 @@ Foreman::Application.routes.draw do
27
27
  post :cancel_step
28
28
  end
29
29
  end
30
- resources :tasks, :only => [:show], constraints: ->(req) { req.format == :csv } do
30
+ resources :tasks, :only => [], constraints: ->(req) { req.format == :csv } do
31
31
  member do
32
32
  get :sub_tasks
33
33
  end
@@ -35,6 +35,7 @@ Foreman::Application.routes.draw do
35
35
  resources :tasks, :only => [:index], constraints: ->(req) { req.format == :csv }
36
36
 
37
37
  match '/tasks', to: '/react#index', via: :get
38
+ match '/tasks/:id', to: '/react#index', via: :get, :as => :task
38
39
  match '/tasks/:id/sub_tasks', to: '/react#index', via: :get
39
40
 
40
41
  namespace :api do
@@ -20,7 +20,7 @@ module ForemanTasks
20
20
  require 'foreman/cron'
21
21
 
22
22
  Foreman::Plugin.register :"foreman-tasks" do
23
- requires_foreman '>= 3.19'
23
+ requires_foreman '>= 5.0'
24
24
  register_global_js_file 'global'
25
25
  divider :top_menu, :parent => :monitor_menu, :last => true, :caption => N_('Foreman Tasks')
26
26
  menu :top_menu, :tasks,
@@ -36,7 +36,7 @@ module ForemanTasks
36
36
  :last => true
37
37
 
38
38
  security_block :foreman_tasks do |_map|
39
- permission :view_foreman_tasks, { :'foreman_tasks/tasks' => [:auto_complete_search, :sub_tasks, :index, :summary, :summary_sub_tasks, :show],
39
+ permission :view_foreman_tasks, { :'foreman_tasks/tasks' => [:auto_complete_search, :sub_tasks, :index, :summary, :summary_sub_tasks],
40
40
  :'foreman_tasks/api/tasks' => [:bulk_search, :show, :index, :summary, :summary_sub_tasks, :details, :sub_tasks] }, :resource_type => 'ForemanTasks::Task'
41
41
  permission :edit_foreman_tasks, { :'foreman_tasks/tasks' => [:resume, :unlock, :force_unlock, :cancel_step, :cancel, :abort],
42
42
  :'foreman_tasks/api/tasks' => [:bulk_resume, :bulk_cancel, :bulk_stop] }, :resource_type => 'ForemanTasks::Task'
@@ -1,3 +1,3 @@
1
1
  module ForemanTasks
2
- VERSION = '12.2.4'.freeze
2
+ VERSION = '13.1.0'.freeze
3
3
  end
@@ -82,15 +82,6 @@ module ForemanTasks
82
82
  assert_include response.body.lines[1], 'Some action'
83
83
  end
84
84
 
85
- describe 'show' do
86
- it 'does not allow user without permissions to see task details' do
87
- setup_user('view', 'foreman_tasks', 'owner.id = current_user')
88
- get :show, params: { id: FactoryBot.create(:some_task).id },
89
- session: set_session_user(User.current)
90
- assert_response :not_found
91
- end
92
- end
93
-
94
85
  describe 'index' do
95
86
  it 'shows duration column' do
96
87
  task = ForemanTasks::Task.select_duration.find(FactoryBot.create(:some_task).id)
@@ -9,8 +9,8 @@ require_relative './support/history_tasks_builder'
9
9
  require 'dynflow/testing'
10
10
  require 'foreman_tasks/test_helpers'
11
11
 
12
- FactoryBot.definition_file_paths = ["#{ForemanTasks::Engine.root}/test/factories"]
13
- FactoryBot.find_definitions
12
+ FactoryBot.definition_file_paths << "#{ForemanTasks::Engine.root}/test/factories"
13
+ FactoryBot.reload
14
14
 
15
15
  ForemanTasks.dynflow.require!
16
16
  ForemanTasks.dynflow.config.disable_active_record_actions = true
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../test_plugin_helper'
4
+ require 'integration_test_helper'
5
+
6
+ class TasksIntegrationTest < IntegrationTestWithJavascript
7
+ test 'does not allow user without permissions to see task details' do
8
+ setup_user('view', 'foreman_tasks', 'owner.id = current_user')
9
+ task = FactoryBot.create(:some_task)
10
+ set_request_user(User.current)
11
+
12
+ visit "/foreman_tasks/tasks/#{task.id}"
13
+
14
+ assert_selector 'h5', text: /Unable to load task/i
15
+ assert_no_selector '#task-details-tabs'
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This calls the main test_helper in Foreman-core
4
+ require 'test_helper'
5
+
6
+ # Add plugin to FactoryBot's paths
7
+ FactoryBot.definition_file_paths << File.join(File.dirname(__FILE__), 'factories')
8
+ FactoryBot.reload
@@ -1,75 +1,13 @@
1
- import React, { useState } from 'react';
2
- import PropTypes from 'prop-types';
1
+ import React from 'react';
2
+ import { Stack, StackItem } from '@patternfly/react-core';
3
3
  import TaskInfo from './TaskInfo';
4
- import {
5
- ForceUnlockConfirmationModal,
6
- UnlockConfirmationModal,
7
- } from '../../common/ClickConfirmation';
8
- import { TaskButtons } from './TaskButtons';
9
4
 
10
- const Task = props => {
11
- const {
12
- taskReload,
13
- id,
14
- forceCancelTaskRequest,
15
- unlockTaskRequest,
16
- action,
17
- taskReloadStart,
18
- } = props;
19
- const forceUnlock = () => {
20
- if (!taskReload) {
21
- taskReloadStart(id);
22
- }
23
- forceCancelTaskRequest(id, action);
24
- };
25
- const unlock = () => {
26
- if (!taskReload) {
27
- taskReloadStart(id);
28
- }
29
- unlockTaskRequest(id, action);
30
- };
31
- const [unlockModalOpen, setUnlockModalOpen] = useState(false);
32
- const [forceUnlockModalOpen, setForceUnlockModalOpen] = useState(false);
33
-
34
- return (
35
- <React.Fragment>
36
- <UnlockConfirmationModal
37
- onClick={unlock}
38
- isOpen={unlockModalOpen}
39
- setModalClosed={() => setUnlockModalOpen(false)}
40
- />
41
- <ForceUnlockConfirmationModal
42
- onClick={forceUnlock}
43
- isOpen={forceUnlockModalOpen}
44
- setModalClosed={() => setForceUnlockModalOpen(false)}
45
- />
46
- <TaskButtons
47
- taskReloadStart={taskReloadStart}
48
- setUnlockModalOpen={setUnlockModalOpen}
49
- setForceUnlockModalOpen={setForceUnlockModalOpen}
50
- {...props}
51
- />
5
+ const Task = props => (
6
+ <Stack hasGutter>
7
+ <StackItem>
52
8
  <TaskInfo {...props} />
53
- </React.Fragment>
54
- );
55
- };
56
-
57
- Task.propTypes = {
58
- taskReload: PropTypes.bool,
59
- id: PropTypes.string,
60
- forceCancelTaskRequest: PropTypes.func,
61
- unlockTaskRequest: PropTypes.func,
62
- action: PropTypes.string,
63
- taskReloadStart: PropTypes.func,
64
- };
65
-
66
- Task.defaultProps = {
67
- taskReload: false,
68
- id: '',
69
- forceCancelTaskRequest: () => null,
70
- unlockTaskRequest: () => null,
71
- action: '',
72
- taskReloadStart: () => null,
73
- };
9
+ </StackItem>
10
+ </Stack>
11
+ );
74
12
 
75
13
  export default Task;
@@ -1,9 +1,12 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { Button, Icon } from '@patternfly/react-core';
4
- import { SyncAltIcon } from '@patternfly/react-icons';
3
+ import { Button, Flex, FlexItem, Icon } from '@patternfly/react-core';
4
+ import { SimpleDropdown } from '@patternfly/react-templates';
5
+ import { EllipsisVIcon, SyncAltIcon } from '@patternfly/react-icons';
5
6
  import { translate as __ } from 'foremanReact/common/I18n';
6
7
 
8
+ import './TaskButtons.scss';
9
+
7
10
  export const TaskButtons = ({
8
11
  canEdit,
9
12
  dynflowEnableConsole,
@@ -30,120 +33,128 @@ export const TaskButtons = ({
30
33
  ? undefined
31
34
  : `dynflow_enable_console ${__('Setting is off')}`;
32
35
 
36
+ const overflowItems = [
37
+ {
38
+ value: 'reload',
39
+ ouiaId: 'task-buttons-reload-button',
40
+ content: (
41
+ <>
42
+ <Icon>
43
+ <SyncAltIcon className={taskReload ? 'spin' : ''} />
44
+ </Icon>
45
+ &nbsp;
46
+ {taskReload ? __('Stop auto-reloading') : __('Start auto-reloading')}
47
+ </>
48
+ ),
49
+ onClick: taskProgressToggle,
50
+ },
51
+ {
52
+ value: 'resume',
53
+ ouiaId: 'task-buttons-resume-button',
54
+ content: __('Resume'),
55
+ onClick: () => {
56
+ if (!taskReload) {
57
+ taskReloadStart(id);
58
+ }
59
+
60
+ resumeTaskRequest(id, action);
61
+ },
62
+ isDisabled: !canEdit || !resumable,
63
+ tooltip: editActionsTitle,
64
+ },
65
+ ...(parentTask
66
+ ? [
67
+ {
68
+ value: 'parent',
69
+ ouiaId: 'task-buttons-parent-button',
70
+ content: __('Parent task'),
71
+ to: `/foreman_tasks/tasks/${parentTask}`,
72
+ },
73
+ ]
74
+ : []),
75
+ ...(hasSubTasks
76
+ ? [
77
+ {
78
+ value: 'subtasks',
79
+ ouiaId: 'task-buttons-subtask-button',
80
+ content: __('Sub tasks'),
81
+ to: `/foreman_tasks/tasks/${id}/sub_tasks`,
82
+ },
83
+ ]
84
+ : []),
85
+ {
86
+ value: 'unlock',
87
+ ouiaId: 'task-buttons-unlock-button',
88
+ content: __('Unlock'),
89
+ onClick: () => setUnlockModalOpen(true),
90
+ isDisabled: !canEdit || state !== 'paused',
91
+ tooltip: editActionsTitle,
92
+ },
93
+ {
94
+ value: 'force-unlock',
95
+ ouiaId: 'task-buttons-force-unlock-button',
96
+ content: __('Force Unlock'),
97
+ onClick: () => setForceUnlockModalOpen(true),
98
+ isDisabled: !canEdit || state === 'stopped',
99
+ tooltip: editActionsTitle,
100
+ },
101
+ ];
102
+
33
103
  return (
34
- <>
35
- <Button
36
- variant="secondary"
37
- ouiaId="task-buttons-reload-button"
38
- className="reload-button"
39
- size="sm"
40
- onClick={taskProgressToggle}
41
- >
42
- <Icon className={taskReload ? 'spin' : ''}>
43
- <SyncAltIcon />
44
- </Icon>
45
- {taskReload ? __('Stop auto-reloading') : __('Start auto-reloading')}
46
- </Button>
47
- <Button
48
- variant="secondary"
49
- ouiaId="task-buttons-dynflow-button"
50
- className="dynflow-button"
51
- size="sm"
52
- component="a"
53
- href={`/foreman_tasks/dynflow/${externalId}`}
54
- isDisabled={!dynflowEnableConsole}
55
- target="_blank"
56
- rel="noopener noreferrer"
57
- >
58
- <span title={dynflowTitle} data-original-title={dynflowTitle}>
59
- {__('Dynflow console')}
60
- </span>
61
- </Button>
62
- <Button
63
- variant="secondary"
64
- ouiaId="task-buttons-resume-button"
65
- className="resume-button"
66
- size="sm"
67
- title={editActionsTitle}
68
- data-original-title={editActionsTitle}
69
- isDisabled={!canEdit || !resumable}
70
- onClick={() => {
71
- if (!taskReload) {
72
- taskReloadStart(id);
73
- }
74
- resumeTaskRequest(id, action);
75
- }}
76
- >
77
- {__('Resume')}
78
- </Button>
79
- <Button
80
- variant="secondary"
81
- ouiaId="task-buttons-cancel-button"
82
- className="cancel-button"
83
- size="sm"
84
- title={editActionsTitle}
85
- data-original-title={editActionsTitle}
86
- isDisabled={!canEdit || !cancellable}
87
- onClick={() => {
88
- if (!taskReload) {
89
- taskReloadStart(id);
90
- }
91
- cancelTaskRequest(id, action);
92
- }}
93
- >
94
- {__('Cancel')}
95
- </Button>
96
- {parentTask && (
104
+ <Flex
105
+ className="task-buttons"
106
+ display={{ default: 'inlineFlex' }}
107
+ alignItems={{ default: 'alignItemsCenter' }}
108
+ spaceItems={{ default: 'spaceItemsSm' }}
109
+ >
110
+ <FlexItem>
97
111
  <Button
98
112
  variant="secondary"
99
- ouiaId="task-buttons-parent-button"
100
- className="parent-button"
113
+ ouiaId="task-buttons-cancel-button"
114
+ className="cancel-button"
101
115
  size="sm"
102
- href={`/foreman_tasks/tasks/${parentTask}`}
103
- component="a"
116
+ title={editActionsTitle}
117
+ data-original-title={editActionsTitle}
118
+ isDisabled={!canEdit || !cancellable}
119
+ onClick={() => {
120
+ if (!taskReload) {
121
+ taskReloadStart(id);
122
+ }
123
+
124
+ cancelTaskRequest(id, action);
125
+ }}
104
126
  >
105
- {__('Parent task')}
127
+ {__('Cancel')}
106
128
  </Button>
107
- )}
108
- {hasSubTasks && (
129
+ </FlexItem>
130
+ <FlexItem>
109
131
  <Button
110
132
  variant="secondary"
111
- ouiaId="task-buttons-subtask-button"
112
- className="subtask-button"
133
+ ouiaId="task-buttons-dynflow-button"
134
+ className="dynflow-button"
113
135
  size="sm"
114
- href={`/foreman_tasks/tasks/${id}/sub_tasks`}
115
136
  component="a"
137
+ href={`/foreman_tasks/dynflow/${externalId}`}
138
+ isDisabled={!dynflowEnableConsole}
139
+ target="_blank"
140
+ rel="noopener noreferrer"
116
141
  >
117
- {__('Sub tasks')}
142
+ <span title={dynflowTitle} data-original-title={dynflowTitle}>
143
+ {__('Dynflow console')}
144
+ </span>
118
145
  </Button>
119
- )}
120
- <Button
121
- variant="secondary"
122
- ouiaId="task-buttons-unlock-button"
123
- className="unlock-button"
124
- size="sm"
125
- isDisabled={!canEdit || state !== 'paused'}
126
- onClick={() => {
127
- setUnlockModalOpen(true);
128
- }}
129
- title={editActionsTitle}
130
- data-original-title={editActionsTitle}
131
- >
132
- {__('Unlock')}
133
- </Button>
134
- <Button
135
- variant="secondary"
136
- ouiaId="task-buttons-force-unlock-button"
137
- className="force-unlock-button"
138
- size="sm"
139
- isDisabled={!canEdit || state === 'stopped'}
140
- onClick={() => setForceUnlockModalOpen(true)}
141
- title={editActionsTitle}
142
- data-original-title={editActionsTitle}
143
- >
144
- {__('Force Unlock')}
145
- </Button>
146
- </>
146
+ </FlexItem>
147
+ <FlexItem>
148
+ <SimpleDropdown
149
+ ouiaId="task-buttons-overflow-dropdown"
150
+ toggleVariant="plain"
151
+ toggleContent={<EllipsisVIcon aria-hidden="true" />}
152
+ popperProps={{ position: 'right' }}
153
+ initialItems={overflowItems}
154
+ toggleProps={{ 'aria-label': __('Task actions') }}
155
+ />
156
+ </FlexItem>
157
+ </Flex>
147
158
  );
148
159
  };
149
160
 
@@ -0,0 +1,45 @@
1
+ .task-buttons {
2
+ .spin {
3
+ -webkit-animation: spin 1s infinite linear;
4
+ -moz-animation: spin 1s infinite linear;
5
+ -o-animation: spin 1s infinite linear;
6
+ animation: spin 1s infinite linear;
7
+ -webkit-transform-origin: 50% 50%;
8
+ transform-origin: 50% 50%;
9
+ -ms-transform-origin: 50% 50%; /* IE 9 */
10
+ }
11
+
12
+ @-moz-keyframes spin {
13
+ from {
14
+ -moz-transform: rotate(0deg);
15
+ }
16
+
17
+ to {
18
+ -moz-transform: rotate(360deg);
19
+ }
20
+ }
21
+
22
+ @-webkit-keyframes spin {
23
+ from {
24
+ -webkit-transform: rotate(0deg);
25
+ }
26
+
27
+ to {
28
+ -webkit-transform: rotate(360deg);
29
+ }
30
+ }
31
+
32
+ @keyframes spin {
33
+ from {
34
+ transform: rotate(0deg);
35
+ }
36
+
37
+ to {
38
+ transform: rotate(360deg);
39
+ }
40
+ }
41
+
42
+ .dynflow-button > span {
43
+ pointer-events: auto;
44
+ }
45
+ }
@@ -22,3 +22,57 @@ export const durationInWords = (
22
22
 
23
23
  const numberWithDelimiter = x =>
24
24
  x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
25
+
26
+ export const isDelayed = ({ startAt, startedAt }) => {
27
+ if (
28
+ startAt == null ||
29
+ startedAt == null ||
30
+ startAt === '' ||
31
+ startedAt === ''
32
+ ) {
33
+ return false;
34
+ }
35
+
36
+ const a = new Date(startAt);
37
+ const b = new Date(startedAt);
38
+
39
+ if (Number.isNaN(a.getTime()) || Number.isNaN(b.getTime())) {
40
+ return false;
41
+ }
42
+
43
+ a.setMilliseconds(0);
44
+ b.setMilliseconds(0);
45
+
46
+ return a.getTime() !== b.getTime();
47
+ };
48
+
49
+ export const parseUsernameLinkHref = usernamePath => {
50
+ if (!usernamePath || typeof usernamePath !== 'string') {
51
+ return null;
52
+ }
53
+
54
+ const match = usernamePath.match(/href=(["'])(.*?)\1/i);
55
+
56
+ return match ? match[2] : null;
57
+ };
58
+
59
+ export const formatTaskDuration = (startedAtStr, endedAtStr, state) => {
60
+ if (state !== 'stopped' || !startedAtStr || !endedAtStr) {
61
+ return __('N/A');
62
+ }
63
+
64
+ const startMs = new Date(startedAtStr).getTime();
65
+ const endMs = new Date(endedAtStr).getTime();
66
+
67
+ if (Number.isNaN(startMs) || Number.isNaN(endMs) || endMs < startMs) {
68
+ return __('N/A');
69
+ }
70
+
71
+ const duration = durationInWords(startedAtStr, endedAtStr);
72
+
73
+ if (typeof duration === 'string') {
74
+ return duration;
75
+ }
76
+
77
+ return duration.text;
78
+ };