Skip to content

Commit

Permalink
Deregister hosts list frontend (#1601)
Browse files Browse the repository at this point in the history
* Set deregistering reducer functions

* Deregister host saga

* Add deregistration button and modal to hosts list view

* Rename selectedHost by hostToDeregister

* Move some logic to named functions
  • Loading branch information
arbulu89 authored Jul 11, 2023
1 parent 68d8614 commit d545559
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 2 deletions.
45 changes: 43 additions & 2 deletions assets/js/components/HostsList.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import React, { useState } from 'react';

import { useSearchParams } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { EOS_WARNING_OUTLINED } from 'eos-icons-react';

import Table from '@components/Table';
import DeregistrationModal from '@components/DeregistrationModal';
import HealthIcon from '@components/Health/HealthIcon';
import Tags from '@components/Tags';
import HostLink from '@components/HostLink';
Expand All @@ -16,8 +17,10 @@ import HealthSummary from '@components/HealthSummary/HealthSummary';
import { getCounters } from '@components/HealthSummary/summarySelection';
import ProviderLabel from '@components/ProviderLabel';
import Tooltip from '@components/Tooltip';
import CleanUpButton from '@components/CleanUpButton';

import { addTagToHost, removeTagFromHost, deregisterHost } from '@state/hosts';

import { addTagToHost, removeTagFromHost } from '@state/hosts';
import { post, del } from '@lib/network';
import { agentVersionWarning } from '@lib/agent';

Expand Down Expand Up @@ -50,9 +53,21 @@ function HostsList() {
);

const [searchParams, setSearchParams] = useSearchParams();
const [cleanUpModalOpen, setCleanUpModalOpen] = useState(false);
const [hostToDeregister, setHostToDeregister] = useState(undefined);

const dispatch = useDispatch();

const openDeregistrationModal = (host) => {
setHostToDeregister(host);
setCleanUpModalOpen(true);
};

const cleanUpHost = (host) => {
setCleanUpModalOpen(false);
dispatch(deregisterHost(host));
};

const config = {
pagination: true,
usePadding: false,
Expand Down Expand Up @@ -177,6 +192,20 @@ function HostsList() {
/>
),
},
{
title: '',
key: 'deregisterable',
className: 'w-48',
render: (content, item) =>
content && (
<CleanUpButton
cleaning={item.deregistering}
onClick={() => {
openDeregistrationModal(item);
}}
/>
),
},
],
};

Expand All @@ -199,13 +228,25 @@ function HostsList() {
id: host.id,
tags: (host.tags && host.tags.map((tag) => tag.value)) || [],
sap_systems: sapSystemList,
deregisterable: host.deregisterable,
deregistering: host.deregistering,
};
});

const counters = getCounters(data || []);
return (
<>
<PageHeader className="font-bold">Hosts</PageHeader>
<DeregistrationModal
hostname={hostToDeregister?.hostname}
isOpen={!!cleanUpModalOpen}
onCleanUp={() => {
cleanUpHost(hostToDeregister);
}}
onCancel={() => {
setCleanUpModalOpen(false);
}}
/>
<div className="bg-white rounded-lg shadow">
<HealthSummary {...counters} className="px-4 py-2" />
<Table
Expand Down
91 changes: 91 additions & 0 deletions assets/js/components/HostsList.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,97 @@ describe('HostsLists component', () => {
});
});

describe('deregistration', () => {
it('should show the clean up button when the host is deregisterable', () => {
const host1 = hostFactory.build({ deregisterable: true });
const host2 = hostFactory.build({ deregisterable: false });
const state = {
...defaultInitialState,
hostsList: {
hosts: [].concat(host1, host2),
},
};

const [StatefulHostsList] = withState(<HostsList />, state);

renderWithRouter(StatefulHostsList);
const table = screen.getByRole('table');
const cleanUpCell1 = table.querySelector(
'tr:nth-child(1) > td:nth-child(9)'
);
const cleanUpCell2 = table.querySelector(
'tr:nth-child(2) > td:nth-child(9)'
);
expect(cleanUpCell1).toHaveTextContent('Clean up');
expect(cleanUpCell2).not.toHaveTextContent('Clean up');
});

it('should show the host in deregistering state', () => {
const host = hostFactory.build({
deregisterable: true,
deregistering: true,
});
const state = {
...defaultInitialState,
hostsList: {
hosts: [host],
},
};

const [StatefulHostsList] = withState(<HostsList />, state);

renderWithRouter(StatefulHostsList);
expect(screen.getByLabelText('Loading')).toBeInTheDocument();
});

it('should request a deregistration when the clean up button in the modal is clicked', async () => {
const user = userEvent.setup();

const host = hostFactory.build({ deregisterable: true });
const state = {
...defaultInitialState,
hostsList: {
hosts: [host],
},
};

const [StatefulHostsList, store] = withState(<HostsList />, state);

renderWithRouter(StatefulHostsList);

const table = screen.getByRole('table');
const cleanUpButton = table.querySelector(
'tr:nth-child(1) > td:nth-child(9) > button'
);

await user.click(cleanUpButton);

expect(
screen.getByText(
`Clean up data discovered by agent on host ${host.hostname}`
)
).toBeInTheDocument();

const cleanUpModalButton = screen.getAllByRole('button', {
name: 'Clean up',
})[1];

await user.click(cleanUpModalButton);

const actions = store.getActions();
const expectedActions = [
{
type: 'DEREGISTER_HOST',
payload: expect.objectContaining({
id: host.id,
hostname: host.hostname,
}),
},
];
expect(actions).toEqual(expect.arrayContaining(expectedActions));
});
});

describe('filtering', () => {
const cleanInitialState = {
hostsList: {
Expand Down
20 changes: 20 additions & 0 deletions assets/js/state/hosts.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ export const hostsListSlice = createSlice({
return host;
});
},
setHostDeregistering: (state, action) => {
state.hosts = state.hosts.map((host) => {
if (host.id === action.payload.id) {
return { ...host, deregistering: true };
}
return host;
});
},
setHostNotDeregistering: (state, action) => {
state.hosts = state.hosts.map((host) => {
if (host.id === action.payload.id) {
return { ...host, deregistering: false };
}
return host;
});
},
startHostsLoading: (state) => {
state.loading = true;
},
Expand All @@ -95,13 +111,15 @@ export const CHECK_HOST_IS_DEREGISTERABLE = 'CHECK_HOST_IS_DEREGISTERABLE';
export const CANCEL_CHECK_HOST_IS_DEREGISTERABLE =
'CANCEL_CHECK_HOST_IS_DEREGISTERABLE';
export const HOST_DEREGISTERED = 'HOST_DEREGISTERED';
export const DEREGISTER_HOST = 'DEREGISTER_HOST';

export const checkHostIsDeregisterable = createAction(
CHECK_HOST_IS_DEREGISTERABLE
);
export const cancelCheckHostIsDeregisterable = createAction(
CANCEL_CHECK_HOST_IS_DEREGISTERABLE
);
export const deregisterHost = createAction(DEREGISTER_HOST);

export const {
setHosts,
Expand All @@ -113,6 +131,8 @@ export const {
setHeartbeatCritical,
setHostListDeregisterable,
setHostNotDeregisterable,
setHostDeregistering,
setHostNotDeregistering,
startHostsLoading,
stopHostsLoading,
removeHost,
Expand Down
30 changes: 30 additions & 0 deletions assets/js/state/hosts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import hostsReducer, {
removeHost,
setHostListDeregisterable,
setHostNotDeregisterable,
setHostDeregistering,
setHostNotDeregistering,
} from '@state/hosts';
import { hostFactory } from '@lib/test-utils/factories';

Expand Down Expand Up @@ -39,6 +41,34 @@ describe('Hosts reducer', () => {
expect(hostsReducer(initialState, action)).toEqual(expectedState);
});

it('should set host in deregistering state', () => {
const [host1, host2] = hostFactory.buildList(2);
const initialState = { hosts: [host1, host2] };

const action = setHostDeregistering(host1);

const expectedState = {
hosts: [{ ...host1, deregistering: true }, host2],
};

expect(hostsReducer(initialState, action)).toEqual(expectedState);
});

it('should remove deregistering state from host', () => {
const [host1, host2] = hostFactory.buildList(2);
const initialState = {
hosts: [host1, host2],
};

const action = setHostNotDeregistering(host1);

const expectedState = {
hosts: [{ ...host1, deregistering: false }, host2],
};

expect(hostsReducer(initialState, action)).toEqual(expectedState);
});

it('should remove host from state', () => {
const [host1, host2] = hostFactory.buildList(2);
const initialState = {
Expand Down
25 changes: 25 additions & 0 deletions assets/js/state/sagas/hosts.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import {
CHECK_HOST_IS_DEREGISTERABLE,
CANCEL_CHECK_HOST_IS_DEREGISTERABLE,
HOST_DEREGISTERED,
DEREGISTER_HOST,
removeHost,
setHostListDeregisterable,
setHostDeregistering,
setHostNotDeregistering,
} from '@state/hosts';

import { del } from '@lib/network';
import { notify } from '@state/actions/notifications';

export function* markDeregisterableHosts(hosts) {
Expand Down Expand Up @@ -43,10 +48,30 @@ export function* hostDeregistered({ payload }) {
);
}

export function* deregisterHost({ payload }) {
yield put(setHostDeregistering(payload));
try {
yield call(del, `/hosts/${payload.id}`);
} catch (error) {
yield put(
notify({
text: `Error deregistering host ${payload?.hostname}.`,
icon: '❌',
})
);
} finally {
yield put(setHostNotDeregistering(payload));
}
}

export function* watchHostDeregisterable() {
yield takeEvery(CHECK_HOST_IS_DEREGISTERABLE, checkHostDeregisterable);
}

export function* watchHostDeregistered() {
yield takeEvery(HOST_DEREGISTERED, hostDeregistered);
}

export function* watchDeregisterHost() {
yield takeEvery(DEREGISTER_HOST, deregisterHost);
}
Loading

0 comments on commit d545559

Please sign in to comment.