-
Notifications
You must be signed in to change notification settings - Fork 42
[#370] Cancel a migration's in-progress tasks #517
[#370] Cancel a migration's in-progress tasks #517
Conversation
6e426f3
to
025b580
Compare
|
||
const cancelSuccessMsg = __('Migration canceled successfully for VMs: '); | ||
const vmNames = []; | ||
tasks.map(task => vmNames.push(task.vmName)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think here, we can just do
const vmNames = tasks.map(task => task.vmName)
const cancelSuccessMsg = __('Migration canceled successfully for VMs: '); | ||
const vmNames = []; | ||
tasks.map(task => vmNames.push(task.vmName)); | ||
const cancelSuccessMsgWithVMNames = `${cancelSuccessMsg} ${vmNames}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor nit, but if we pass an array (with multiple entries) into the template literal, there will not be spaces after each comma
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, it might be worth doing something like this:
const vmNamesStr = vmNames.join(', ');
const cancelSuccessMsgWithVMNames = `${cancelSuccessMsg} ${vmNamesStr}`;
edit: to combine with the above comment, it could all just be:
const vmNames = tasks.map(task => task.vmName).join(', ');
const cancelSuccessMsgWithVMNames = `${cancelSuccessMsg} ${vmNames}`;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the tip.
Addressed this in the last commit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The bulk selector gets checked after canceling a task (this is actually observable in the first screenshot). Steps to reproduce
- Cancel a task
- Notice that the bulk selector is now checked
// gather the selected tasks in state and feed them through | ||
const { selectedCancelTasks } = this.state; | ||
const { cancelPlanRequestTasksAction, cancelPlanRequestTasksUrl } = this.props; | ||
cancelPlanRequestTasksAction(cancelPlanRequestTasksUrl, selectedCancelTasks); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might want to refetch the tasks here. Currently, after confirming the cancel, the success notification appears, but we need to wait for the next poll before the ListItem is updated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did not do a refetch here.
The listview reflects the cancelled state immediately upon a successful return from the API POST cancel
action.
I have used markedForCancellation
redux state to mark the cancelled items in the listview before the task is officially marked with cancel_requested=true
by the API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Observation about the bulk selector & checkbox state after cancellation. Does not affect functionality, but wondering what the intended behavior is for this. Steps to reproduce
- Cancel a task
- Notice that the checkbox for the ListItem is disabled, but checked
- Using the bulk selector, unselect all
- Now the checkbox for the ListItem is not checked
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to comment
Can attempt to cancel a already cancelled task. Steps to reproduce
- Cancel a task. Notice that the checkbox for the ListItem is disabled, but checked.
- Attempt to cancel another task
- In the confirm modal, the already cancelled plan appears along with the new one
@michaelkro I think the expected behavior should be -
Let me know if you can think of more. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eeekkkk so trying this out.. latest miq master its not a happy camper...
a post to /api/request_tasks
with the body {"action":"cancel","resources":[{"id":"173"}]}
returns {"error":{"kind":"bad_request","message":"Unsupported Action cancel for the request_tasks collection specified","klass":"Api::BadRequestError"}}
Maybe there's a branch somewhere that i neeeeed to be on?
@@ -29,7 +30,8 @@ export const STATUS_MESSAGE_KEYS = { | |||
VALIDATING: 'Validating', | |||
VM_MIGRATIONS_COMPLETED: 'VM Transformations completed', | |||
VM_MIGRATED: 'Virtual machine migrated', | |||
VM_MIGRATIONS_FAILED: 'VM Transformations failed' | |||
VM_MIGRATIONS_FAILED: 'VM Transformations failed', | |||
CANCELLED: 'VM cancelled' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe want to say VM Migration canceled
here and elsewhere...we follow the pattern of item/action/result elsewhere 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have removed this constant for now, since we don't know yet what string the state machine actually uses for a cancelled VM.
This will become more clear after some appliance testing and once Fabien's cancellation implementation is complete
@@ -79,6 +81,10 @@ const processVMTasks = vmTasks => { | |||
taskDetails.options.playbooks = task.options.playbooks; | |||
} | |||
|
|||
if (task.options.cancel_requested) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be done on the item creation above, say line 69,
cancel_requested: task.options.cancel_requested
(but I don't know what the value of that attribute is, so maybe direct assignment doesn't make sense)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in the last commit
</React.Fragment> | ||
} | ||
cancelButtonLabel={__('No')} | ||
confirmButtonLabel={__('Cancel Migrations')} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmation buttons thus far have been single word actions, without specifying the item on which the action is taken, I know that this might have been specified in the mock... but it seems to introduce a design inconsistency @vconzola thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AllenBW This is a classic UX conundrum - how to confirm an action when that action is cancelling something. The normal button labels (OK, Cancel) lead to confusion - Does Cancel cancel or cancel the Cancel. So I ran this by Stacey (our info architect) and this is what we came up with. Maybe not consistent, but in this case necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AllenBW @vconzola Just my $0.02, but I feel like the single-word restriction is specifically confusing in this "cancelling the cancel" case. I've been happy with products that I see use the opposite tactic when one word isn't enough-- very explicit and verbose buttons.
"Yes, cancel these migrations" / "No, do not cancel anything" or something to that effect.
I think using one word where it's not confusing, and then just going straight for a sentence otherwise instead of trying to find the two word button that works, is good. End of non-designer opinion.
@AllenBW You will need ManageIQ/manageiq-api#421 |
Ahhhh that's the missing piece, thanks @AparnaKarve ! |
const { selectedCancelTasks } = this.state; | ||
const { cancelPlanRequestTasksAction, cancelPlanRequestTasksUrl } = this.props; | ||
cancelPlanRequestTasksAction(cancelPlanRequestTasksUrl, selectedCancelTasks); | ||
this.setState({ showConfirmCancel: false }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Modal window isn't hiding after firing this 😕
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you check again? The modal seems to be hiding OK for me.
It might be something wacky I did... or desired behavior... but after canceling two in progress migrations... they get the plan appears to be stuck in inprogress purgatory... meaning... it just hangs out inprogress tab... with no action aside from download logs available |
I have noticed some strange behavior with Cancel locally as well... hence I think the real valid test of canceling a migration should be done on an appliance - it seemed to work a lot better on an appliance yesterday. So based on the testing that can be done locally, I will try and fix some of the UI/UX issues that @michaelkro noticed above, but the bulk of the testing needs to be done on an appliance. I will try and setup an appliance sometime today for testing this. |
@michaelkro I concur with @AparnaKarve expected behavior comment above. |
Working on some of the quirky behavior around the checkboxes, since it's related to stuff I did. I'll have a commit for @AparnaKarve to cherry-pick shortly |
Posting this here for posterity, it might conflict with @AparnaKarve's redux stuff, but it should fix some behavior around the bulk selector: mturley@a65868d |
After implementing the redux approach, came across a few use cases where the user can sneak in a Cancel on an already canceled task (in the brief window until the next polling happens) Trying to address some of these cases... |
12b7d3f
to
7c28590
Compare
@michaelkro @AllenBW @mturley Please test/review when you get a chance.
@AllenBW We usually don't see this issue on appliances... although I have tried to address this in c3fe254, I will try and setup an appliance to test this, but note that, currently migrations are failing on the appliances in the "Pre-Migration" step - we need an automate PR to fix this |
Note for future reference: `taskCancelled` indicates that a task has already been cancelled (inferred from the API output) whereas `taskCancelling` indicates a task that was marked for cancellation in the UI Both states will display the `fa-ban` icon in the listview and display the text `Canceled Migration` for the task
- Remove completed task from selection - Apply cancellation for incomplete tasks only
unknown status messages are usually about stalled migrations, hence the status message - `VM migrations stalled`
Thank you so much for your hard work on this @AparnaKarve. |
export const cancelPlanRequestTasksAction = (url, tasks) => dispatch => { | ||
const completedTasks = []; | ||
const incompleteTasks = []; | ||
tasks.map(t => (t.completed ? completedTasks.push(t) : incompleteTasks.push(t))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this ultimately has the desired effect, I think we may want to use forEach
here. In my experience, we use map
when we want to do something with the returned array. In this case, it seems that we are iterating through tasks
(rather than mapping it to a new array to be used later)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Beat me to it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in the last commit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not going to request changes, because these are pretty minor nit picks for readability.
export const cancelPlanRequestTasksAction = (url, tasks) => dispatch => { | ||
const completedTasks = []; | ||
const incompleteTasks = []; | ||
tasks.map(t => (t.completed ? completedTasks.push(t) : incompleteTasks.push(t))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you don't use map
's return value here, it could be a forEach
instead.. but it really doesn't matter, and if this is the only thing I can find to comment on, I think your code looks great!
@@ -50,21 +56,32 @@ const excludeDownloadDoneTaskId = (allDownloadLogInProgressTaskIds, taskId) => | |||
const includeDownloadInProgressTaskId = (allDownloadLogInProgressTaskIds, taskId) => | |||
allDownloadLogInProgressTaskIds ? allDownloadLogInProgressTaskIds.concat(taskId) : [taskId]; | |||
|
|||
const removeCancelledTasksFromSelection = (allSelectedTasksForCancel, alreadyCancelledTasks) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine, but a little odd looking, and will run in n^2 time (I think? been a while since big-O lectures). Here's my approach on this one, taking the map out and only using one filter
, and then using every
which will short circuit if it sees a non-matching element.
Not tested, but:
const removeCancelledTasksFromSelection = (allSelectedTasksForCancel, alreadyCancelledTasks) => (
allSelectedTasksForCancel.filter(selectedTask =>
alreadyCancelledTasks.every(cancelledTask => selectedTask.id !== cancelledTask.id)
)
);
Not that we need to optimize for runtime here, but I think that's more readable too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion.
I have applied this change in the last commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@michaelkro I think that's the bug I thought we were seeing at first yesterday, which should be fixed by my commit here in the old implementation: mturley@a65868d. @AparnaKarve I agree that it's not a blocker, but you could port this line from my commit into your logic to fix this.
(it's the |
return { | ||
filteredTasks, | ||
incompleteTasks, | ||
allSelected: selectedTasksForCancel.length === incompleteTasks.length, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, @AparnaKarve I didn't notice you based your work on a slightly older version of getCancelSelectionState
. I pushed a newer one on Wednesday that fixed the bug @michaelkro was just reporting (select all checkbox is checked when there are no selectable tasks).
We just need to pull noneSelected
up into a variable, and then do allSelected: !noneSelected && ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed the bulk selector issues in the last commit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LG2M!!
Any final input @michaelkro @mturley ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The resorting thing is the only thing that I'm a little concerned about, but it could be due to local environment quirks. I'm ok with handling that as a separate issue
Ok, so just a reminder that we need the API PR merged in first before this. While I have some time until the API PR is merged, let me check what more I can address from the above comments... @michaelkro The resorting thing is independent of this PR (KK also mentioned to me about it once) - so yes, it could be handled as a separate issue. |
Code all looks good to me, I haven't tested your new commits locally yet since yesterday morning though. While you're wrapping up I'll see if I can find any more issues. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Thanks again @AparnaKarve @michaelkro and @priley86.
7c28590
to
5f9c89b
Compare
Thanks everyone for the reviews and testing! I have done yet another round of addressing feedback - 5f9c89b Currently keeping an eye on the API PR... will let you all know when this can be merged. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Thanks so much for taking this over @AparnaKarve .
(allSelectedTasksForCancel = allSelectedTasksForCancel.filter(element => element.id !== cancelledTask.id)) | ||
const removeCancelledTasksFromSelection = (allSelectedTasksForCancel, alreadyCancelledTasks) => | ||
allSelectedTasksForCancel.filter(selectedTask => | ||
alreadyCancelledTasks.every(cancelledTask => selectedTask.id !== cancelledTask.id) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 I love the lesser-known Array
methods :)
@@ -285,7 +285,7 @@ class PlanRequestDetailList extends React.Component { | |||
return { | |||
filteredTasks, | |||
incompleteTasks, | |||
allSelected: selectedTasksForCancel.length === incompleteTasks.length, | |||
allSelected: selectedTasksForCancel.length === incompleteTasks.length && selectedTasksForCancel.length > 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
…igrations [ManageIQ#370] Cancel a migration's in-progress tasks
Backported to Gaprindashvili via #525 |
Fixes #370
This is a continuation to @priley86 's and @mturley 's work...
Includes -
fa-ban
icon for canceled tasksCancel Migration
button state appropriatelyhttps://bugzilla.redhat.com/show_bug.cgi?id=1564257
To be merged after ManageIQ/manageiq-api#421