-
Notifications
You must be signed in to change notification settings - Fork 1k
Fixes #38416 - Hosts bulk action : Assign organization/ location #10538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -38,4 +38,19 @@ def rebuild_configuration | |||||||||||||||
| end | ||||||||||||||||
| all_fails | ||||||||||||||||
| end | ||||||||||||||||
|
|
||||||||||||||||
| # @param [Boolean] optimistic_import | ||||||||||||||||
| # either fix on mismatch or fail on mismatch | ||||||||||||||||
| def assign_taxonomy(taxonomy, optimistic_import) | ||||||||||||||||
| tax_type = taxonomy.type.downcase | ||||||||||||||||
| if optimistic_import | ||||||||||||||||
| @hosts.update_all("#{tax_type}_id".to_sym => taxonomy.id) | ||||||||||||||||
| # hosts location needs to be updated before import missing ids | ||||||||||||||||
| taxonomy.import_missing_ids | ||||||||||||||||
|
Comment on lines
47
to
49
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can consistently make it return the number of updated hosts:
Suggested change
Then you can also update the documentation of this method with: # @return [Integer]
# The number of updated hostsYou can then use that in the bulk controller to accurately state how many hosts were updated. However, that also presents another problem: you can't simply add the 2 numbers and they may have overlap. That again stresses why it would be better to do both updates in a single transaction and only update the hosts once. I'd expect it to be faster and also gives a more accurate summary to the user.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Isn't this the same as
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently there is no problem to find out how many hosts are updated accurately. Not sure what the concern is here. It is really hard to make the both updates in one transaction considering the two different mismatch settings and the support for both API versions. And I'm not sure if If it really worth to make it one transaction and it is doable, maybe we can do it in a followup.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No, it's not. So it is at least 0 (in case nothing was updated) or at most
That's because you misunderstood the return value of
The last point is an interesting challenge. But if you can't, then combining both into a single frontend dialog may be wrong to start with.
I think it is about returning a correct number. If you don't want to show a correct number, then please don't show one at all (like the old UI).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will change the message to not display number of hosts.
I want to make it right. Do you think it is reasonable to have 2 tasks for change organization and change location instead of 1 task here in this PR? @ekohl @jeremylenz
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @parthaa
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think having separate modals for change org & change location would be fine, especially since it simplifies our implementation here. I can't imagine a lot of users wanting to do both at the same time, but if I am wrong, it's a rare-enough task that it's worth the tradeoff.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Today it's also separate, so it's the most straight forward migration for the user. |
||||||||||||||||
| elsif taxonomy.need_to_be_selected_ids.empty? | ||||||||||||||||
| @hosts.update_all("#{tax_type}_id".to_sym => taxonomy.id) | ||||||||||||||||
| else | ||||||||||||||||
| raise "Cannot update #{taxonomy.type.downcase} to #{taxonomy.name} because of mismatch in settings" | ||||||||||||||||
| end | ||||||||||||||||
| end | ||||||||||||||||
| end | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,257 @@ | ||
| import React, { useState, useEffect } from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import { useDispatch, useSelector } from 'react-redux'; | ||
| import { | ||
| Modal, | ||
| Button, | ||
| MenuToggle, | ||
| SelectOption, | ||
| TextContent, | ||
| Text, | ||
| } from '@patternfly/react-core'; | ||
| import { addToast } from '../../../ToastsList/slice'; | ||
| import { translate as __ } from '../../../../common/I18n'; | ||
| import { STATUS } from '../../../../constants'; | ||
| import { | ||
| selectAPIStatus, | ||
| selectAPIResponse, | ||
| } from '../../../../redux/API/APISelectors'; | ||
| import { | ||
| BULK_ASSIGN_TAXONOMY_KEY, | ||
| bulkAssignTaxonomy, | ||
| fetchOrganizations, | ||
| fetchLocations, | ||
| ORGANIZATION_KEY, | ||
| LOCATION_KEY, | ||
| } from './actions'; | ||
| import { foremanUrl } from '../../../../common/helpers'; | ||
| import { APIActions } from '../../../../redux/API'; | ||
| import { | ||
| HOSTS_API_PATH, | ||
| API_REQUEST_KEY, | ||
| } from '../../../../routes/Hosts/constants'; | ||
| import TaxonomySelect from './TaxonomySelect'; | ||
|
|
||
| const BulkAssignTaxonomyModal = ({ | ||
| isOpen, | ||
| closeModal, | ||
| selectedCount, | ||
| fetchBulkParams, | ||
| }) => { | ||
| const dispatch = useDispatch(); | ||
| const [organizationId, setOrganizationId] = useState(''); | ||
| const [locationId, setLocationId] = useState(''); | ||
| const [orgSelectOpen, setOrgSelectOpen] = useState(false); | ||
| const [locSelectOpen, setLocSelectOpen] = useState(false); | ||
| const [orgFixRadioChecked, setOrgFixRadioChecked] = useState(true); | ||
| const [locFixRadioChecked, setLocFixRadioChecked] = useState(true); | ||
| const organizations = useSelector(state => | ||
| selectAPIResponse(state, ORGANIZATION_KEY) | ||
| ); | ||
| const organizationStatus = useSelector(state => | ||
| selectAPIStatus(state, ORGANIZATION_KEY) | ||
| ); | ||
| const locations = useSelector(state => | ||
| selectAPIResponse(state, LOCATION_KEY) | ||
| ); | ||
| const locationStatus = useSelector(state => | ||
| selectAPIStatus(state, LOCATION_KEY) | ||
| ); | ||
| const hostUpdateStatus = useSelector(state => | ||
| selectAPIStatus(state, BULK_ASSIGN_TAXONOMY_KEY) | ||
| ); | ||
| const handleModalClose = () => { | ||
| setOrganizationId(''); | ||
| setLocationId(''); | ||
| setOrgFixRadioChecked(true); | ||
| setLocFixRadioChecked(true); | ||
| closeModal(); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| dispatch(fetchOrganizations()); | ||
| dispatch(fetchLocations()); | ||
| }, [dispatch]); | ||
|
|
||
| const onOrgToggleClick = () => { | ||
| setOrgSelectOpen(!orgSelectOpen); | ||
| }; | ||
| const toggleOrg = toggleRef => ( | ||
| <MenuToggle | ||
| ref={toggleRef} | ||
| onClick={onOrgToggleClick} | ||
| isExpanded={orgSelectOpen} | ||
| style={{ width: '95%' }} | ||
| > | ||
| {getSelectedLabel(organizationId, organizations)} | ||
| </MenuToggle> | ||
| ); | ||
|
|
||
| const handleOrgSelect = (event, selection) => { | ||
| setOrganizationId(selection); | ||
| setOrgSelectOpen(false); | ||
| }; | ||
|
|
||
| const onLocToggleClick = () => { | ||
| setLocSelectOpen(!locSelectOpen); | ||
| }; | ||
| const toggleLoc = toggleRef => ( | ||
| <MenuToggle | ||
| ref={toggleRef} | ||
| onClick={onLocToggleClick} | ||
| isExpanded={locSelectOpen} | ||
| style={{ width: '95%' }} | ||
| > | ||
| {getSelectedLabel(locationId, locations)} | ||
| </MenuToggle> | ||
| ); | ||
|
|
||
| const handleLocSelect = (event, selection) => { | ||
| setLocationId(selection); | ||
| setLocSelectOpen(false); | ||
| }; | ||
|
|
||
| const getSelectedLabel = (id, taxonomy) => | ||
| taxonomy.results.find(t => t.id === id)?.name; | ||
|
|
||
| const handleError = error => { | ||
| const { | ||
| response: { | ||
| data: { | ||
| error: { message }, | ||
| }, | ||
| }, | ||
| } = error; | ||
| dispatch(addToast({ type: 'danger', message })); | ||
| handleModalClose(); | ||
| }; | ||
|
|
||
| const handleSuccess = response => { | ||
| dispatch( | ||
| addToast({ | ||
| type: 'success', | ||
| message: response.data.message, | ||
| }) | ||
| ); | ||
| dispatch( | ||
| APIActions.get({ | ||
| key: API_REQUEST_KEY, | ||
| url: foremanUrl(HOSTS_API_PATH), | ||
| }) | ||
| ); | ||
| handleModalClose(); | ||
| }; | ||
|
|
||
| const handleSave = () => { | ||
| const requestBody = { | ||
| included: { | ||
| search: fetchBulkParams(), | ||
| }, | ||
| target_location_id: locationId, | ||
| target_organization_id: organizationId, | ||
| mismatch_setting_location: locFixRadioChecked, | ||
| mismatch_setting_organization: orgFixRadioChecked, | ||
| }; | ||
|
|
||
| dispatch(bulkAssignTaxonomy(requestBody, handleSuccess, handleError)); | ||
| }; | ||
|
|
||
| const modalActions = [ | ||
| <Button | ||
| key="add" | ||
| ouiaId="bulk-assign-taxonomy-modal-add-button" | ||
| variant="primary" | ||
| onClick={handleSave} | ||
| isDisabled={ | ||
| hostUpdateStatus === STATUS.PENDING || | ||
| (organizationId === '' && locationId === '') | ||
| } | ||
| isLoading={hostUpdateStatus === STATUS.PENDING} | ||
| > | ||
| {__('Save')} | ||
| </Button>, | ||
| <Button | ||
| key="cancel" | ||
| ouiaId="bulk-assign-taxonomy-modal-cancel-button" | ||
| variant="link" | ||
| onClick={handleModalClose} | ||
| > | ||
| {__('Cancel')} | ||
| </Button>, | ||
| ]; | ||
| return ( | ||
| <Modal | ||
| isOpen={isOpen} | ||
| onClose={handleModalClose} | ||
| onEscapePress={handleModalClose} | ||
| title={__('Change organization/location')} | ||
| width="50%" | ||
| position="top" | ||
| actions={modalActions} | ||
| id="bulk-assign-taxonomy-modal" | ||
| key="bulk-assign-taxonomy-modal" | ||
| ouiaId="bulk-assign-taxonomy-modal" | ||
| > | ||
| <TextContent> | ||
| <Text ouiaId="bulk-assign-taxonomy-options"> | ||
| {__( | ||
| 'Select organization/location to add hosts to. This change may affect all your selected hosts.' | ||
| )} | ||
| </Text> | ||
| </TextContent> | ||
| {organizations && organizationStatus === STATUS.RESOLVED && ( | ||
| <TaxonomySelect | ||
| headerText={__('Select organization')} | ||
| taxonomy="organization" | ||
| isOpen={orgSelectOpen} | ||
| selected={organizationId} | ||
| onSelect={handleOrgSelect} | ||
| onOpenChange={isSelectOpen => setOrgSelectOpen(isSelectOpen)} | ||
| toggle={toggleOrg} | ||
| radioChecked={orgFixRadioChecked} | ||
| setRadioChecked={setOrgFixRadioChecked} | ||
| > | ||
| {organizations.results?.map(org => ( | ||
| <SelectOption key={org.id} value={org.id}> | ||
| {org.name} | ||
| </SelectOption> | ||
| ))} | ||
| </TaxonomySelect> | ||
| )} | ||
| <hr /> | ||
| {locations && locationStatus === STATUS.RESOLVED && ( | ||
| <TaxonomySelect | ||
| headerText={__('Select location')} | ||
| taxonomy="location" | ||
| isOpen={locSelectOpen} | ||
| selected={locationId} | ||
| onSelect={handleLocSelect} | ||
| onOpenChange={isSelectOpen => setLocSelectOpen(isSelectOpen)} | ||
| toggle={toggleLoc} | ||
| radioChecked={locFixRadioChecked} | ||
| setRadioChecked={setLocFixRadioChecked} | ||
| > | ||
| {locations.results?.map(loc => ( | ||
| <SelectOption key={loc.id} value={loc.id}> | ||
| {loc.name} | ||
| </SelectOption> | ||
| ))} | ||
| </TaxonomySelect> | ||
| )} | ||
| </Modal> | ||
| ); | ||
| }; | ||
|
|
||
| BulkAssignTaxonomyModal.propTypes = { | ||
| isOpen: PropTypes.bool, | ||
| closeModal: PropTypes.func, | ||
| selectedCount: PropTypes.number.isRequired, | ||
| fetchBulkParams: PropTypes.func.isRequired, | ||
| }; | ||
|
|
||
| BulkAssignTaxonomyModal.defaultProps = { | ||
| isOpen: false, | ||
| closeModal: () => {}, | ||
| }; | ||
|
|
||
| export default BulkAssignTaxonomyModal; |
Uh oh!
There was an error while loading. Please reload this page.