Merge pull request #11791 from AlexSCorey/11713-PreventDisassociateHybridNodeFromControlplan

Prevents disassociate hybrid node on controlplane instance group
This commit is contained in:
Alex Corey
2022-03-31 10:34:21 -04:00
committed by GitHub
12 changed files with 77 additions and 63 deletions

View File

@@ -18,6 +18,7 @@ function DisassociateButton({
modalTitle = t`Disassociate?`,
onDisassociate,
verifyCannotDisassociate = true,
isProtectedInstanceGroup = false,
}) {
const [isOpen, setIsOpen] = useState(false);
const { isKebabified, onKebabModalChange } = useContext(KebabifiedContext);
@@ -37,7 +38,10 @@ function DisassociateButton({
return !item.summary_fields?.user_capabilities?.delete;
}
function cannotDisassociateInstances(item) {
return item.node_type === 'control';
return (
item.node_type === 'control' ||
(isProtectedInstanceGroup && item.node_type === 'hybrid')
);
}
const cannotDisassociate = itemsToDisassociate.some(
@@ -73,11 +77,7 @@ function DisassociateButton({
let isDisabled = false;
if (verifyCannotDisassociate) {
isDisabled =
itemsToDisassociate.length === 0 ||
itemsToDisassociate.some(cannotDisassociate);
} else {
isDisabled = itemsToDisassociate.length === 0;
isDisabled = itemsToDisassociate.some(cannotDisassociate);
}
// NOTE: Once PF supports tooltips on disabled elements,
@@ -89,7 +89,7 @@ function DisassociateButton({
<DropdownItem
key="add"
aria-label={t`disassociate`}
isDisabled={isDisabled}
isDisabled={isDisabled || !itemsToDisassociate.length}
component="button"
ouiaId="disassociate-tooltip"
onClick={() => setIsOpen(true)}
@@ -108,7 +108,7 @@ function DisassociateButton({
variant="secondary"
aria-label={t`Disassociate`}
onClick={() => setIsOpen(true)}
isDisabled={isDisabled}
isDisabled={isDisabled || !itemsToDisassociate.length}
>
{t`Disassociate`}
</Button>

View File

@@ -124,5 +124,21 @@ describe('<DisassociateButton />', () => {
);
expect(wrapper.find('button[disabled]')).toHaveLength(1);
});
test('should disable button when selected items contain instances thaat are hybrid and are inside a protected instances', () => {
const wrapper = mountWithContexts(
<DisassociateButton
onDisassociate={() => {}}
isProectedInstanceGroup
itemsToDelete={[
{
id: 1,
hostname: 'awx',
node_type: 'control',
},
]}
/>
);
expect(wrapper.find('button[disabled]')).toHaveLength(1);
});
});
});

View File

@@ -275,10 +275,11 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
</Tooltip>
{me.is_superuser && instance.node_type !== 'control' && (
<DisassociateButton
verifyCannotDisassociate={!me.is_superuser}
verifyCannotDisassociate={instanceGroup.name === 'controlplane'}
key="disassociate"
onDisassociate={disassociateInstance}
itemsToDisassociate={[instance]}
isProtectedInstanceGroup={instanceGroup.name === 'controlplane'}
modalTitle={t`Disassociate instance from instance group?`}
/>
)}

View File

@@ -31,7 +31,7 @@ const QS_CONFIG = getQSConfig('instance', {
order_by: 'hostname',
});
function InstanceList() {
function InstanceList({ instanceGroup }) {
const [isModalOpen, setIsModalOpen] = useState(false);
const location = useLocation();
const { id: instanceGroupId } = useParams();
@@ -224,13 +224,15 @@ function InstanceList() {
]
: []),
<DisassociateButton
verifyCannotDisassociate={selected.some(
(s) => s.node_type === 'control'
)}
verifyCannotDisassociate={
selected.some((s) => s.node_type === 'control') ||
instanceGroup.name === 'controlplane'
}
key="disassociate"
onDisassociate={handleDisassociate}
itemsToDisassociate={selected}
modalTitle={t`Disassociate instance from instance group?`}
isProtectedInstanceGroup={instanceGroup.name === 'controlplane'}
/>,
<HealthCheckButton
isDisabled={!canAdd}

View File

@@ -123,7 +123,7 @@ describe('<InstanceList/>', () => {
await act(async () => {
wrapper = mountWithContexts(
<Route path="/instance_groups/:id/instances">
<InstanceList />
<InstanceList instanceGroup={{ name: 'Alex' }} />
</Route>,
{
context: {

View File

@@ -21,7 +21,7 @@ function Instances({ setBreadcrumb, instanceGroup }) {
/>
</Route>
<Route key="instanceList" path="/instance_groups/:id/instances">
<InstanceList />
<InstanceList instanceGroup={instanceGroup} />
</Route>
</Switch>
);

View File

@@ -186,8 +186,3 @@ export function regExp() {
return undefined;
};
}
export function protectedResourceName(message, names = []) {
return (value) =>
names.some((name) => value.trim() === `${name}`) ? message : undefined;
}

View File

@@ -12,7 +12,6 @@ import {
regExp,
requiredEmail,
validateTime,
protectedResourceName,
} from './validators';
describe('validators', () => {
@@ -188,21 +187,4 @@ describe('validators', () => {
expect(validateTime()('12.15 PM')).toEqual('Invalid time format');
expect(validateTime()('12;15 PM')).toEqual('Invalid time format');
});
test('protectedResourceName should validate properly', () => {
expect(
protectedResourceName('failed validation', ['Alex'])('Apollo')
).toBeUndefined();
expect(
protectedResourceName('failed validation', ['Alex', 'Athena'])('alex')
).toBeUndefined();
expect(
protectedResourceName('failed validation', ['Alex', 'Athena'])('Alex')
).toEqual('failed validation');
expect(
protectedResourceName('failed validation', ['Alex'])('Alex')
).toEqual('failed validation');
expect(
protectedResourceName('failed validation', ['Alex'])('Alex ')
).toEqual('failed validation');
});
});