foreman-tasks 13.0.0 → 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.
- checksums.yaml +4 -4
- data/app/services/foreman_tasks/troubleshooting_help_generator.rb +1 -1
- data/lib/foreman_tasks/version.rb +1 -1
- data/webpack/ForemanTasks/Components/TaskDetails/Components/Task.js +8 -70
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.js +115 -104
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskButtons.scss +45 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskHelper.js +54 -0
- data/webpack/ForemanTasks/Components/TaskDetails/Components/TaskInfo.js +236 -131
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/Task.test.js +27 -33
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskButtons.test.js +83 -67
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskHelper.test.js +118 -1
- data/webpack/ForemanTasks/Components/TaskDetails/Components/__tests__/TaskInfo.test.js +190 -20
- data/webpack/ForemanTasks/Components/TaskDetails/ExecutionDetails.js +65 -0
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.js +48 -51
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetails.scss +0 -39
- data/webpack/ForemanTasks/Components/TaskDetails/TaskDetailsSelectors.js +0 -5
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/ExecutionDetails.test.js +194 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.fixtures.js +99 -0
- data/webpack/ForemanTasks/Components/TaskDetails/__tests__/TaskDetails.test.js +129 -8
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.js +174 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsHeader.scss +5 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/TaskDetailsPage.js +10 -33
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsHeader.test.js +142 -0
- data/webpack/ForemanTasks/Routes/ShowTaskDetails/__tests__/TaskDetailsPage.test.js +100 -88
- data/webpack/ForemanTasks/{Components/TaskDetails → Routes/ShowTaskDetails}/index.js +7 -8
- data/webpack/Routes/routes.js +1 -1
- data/webpack/Routes/routes.test.js +1 -1
- data/webpack/index.js +0 -5
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 270e537ec64af9327b2598da48e8422a56adc1716e45ee5105cb57c9bd2d1078
|
|
4
|
+
data.tar.gz: da3627e82bdecfcd63516cf3e973461b22762023374ee58ac105e7f9e66c2cf7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc37e5ebbdb2aa62a131d2828a619f00c68b1c6c262a78df83671801b7047eeafd2bd31fd43e52fff79d7a9b2bb69267a950bd2e71babc6569c20d55a90caab2
|
|
7
|
+
data.tar.gz: ba180fe7ad221035c14474ed9b1cc62795d2d90ff585fdc0304bda52432e43917829d016abd03e18e68ffcb9654f1a73864695d93fe1219214f7a14d43de0033
|
|
@@ -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 '
|
|
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)
|
|
@@ -1,75 +1,13 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
</
|
|
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 {
|
|
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
|
+
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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-
|
|
100
|
-
className="
|
|
113
|
+
ouiaId="task-buttons-cancel-button"
|
|
114
|
+
className="cancel-button"
|
|
101
115
|
size="sm"
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
{__('
|
|
127
|
+
{__('Cancel')}
|
|
106
128
|
</Button>
|
|
107
|
-
|
|
108
|
-
|
|
129
|
+
</FlexItem>
|
|
130
|
+
<FlexItem>
|
|
109
131
|
<Button
|
|
110
132
|
variant="secondary"
|
|
111
|
-
ouiaId="task-buttons-
|
|
112
|
-
className="
|
|
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
|
-
{
|
|
142
|
+
<span title={dynflowTitle} data-original-title={dynflowTitle}>
|
|
143
|
+
{__('Dynflow console')}
|
|
144
|
+
</span>
|
|
118
145
|
</Button>
|
|
119
|
-
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
};
|