Add host modal: Select team dropdown (#1740)

* Fix Add host modal dropdown for team maintainer

* Update Cypress tests

* Set default values for add host team dropdown
This commit is contained in:
gillespi314 2021-08-20 23:00:44 -05:00 committed by GitHub
parent bd38cc1fe9
commit 8b4c6a1dd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 105 additions and 41 deletions

View File

@ -0,0 +1,2 @@
Add user role validations to select team dropdown in "Add host" modal
Remove Redux enrollSecret state from ManageHostsPage and instead handle as local state in "Add host" modal

View File

@ -23,13 +23,14 @@ describe("Hosts page", () => {
cy.contains("button", /add new host/i).click();
cy.get('a[href*="showSecret"]').click();
cy.contains("a", /download/i)
.first()
.click();
cy.get('a[href*="showSecret"]').click();
// Assert enroll secret downloaded matches the one displayed
// NOTE: This test often fails when the Cypress downloads folder was not cleared properly
// before each test run (seems to be related to issues with Cypress trashAssetsBeforeRun)
cy.readFile(path.join(Cypress.config("downloadsFolder"), "secret.txt"), {
timeout: 5000,
}).then((contents) => {

View File

@ -30,7 +30,7 @@ describe("Basic tier - Admin user", () => {
// See the “Select a team for this new host” in the Add new host modal. This modal appears after the user selects the “Add new host” button
cy.get(".add-host-modal__team-dropdown-wrapper .Select-control").click();
cy.get(".add-host-modal__team-dropdown-wrapper").within(() => {
cy.get(".Select-menu-outer").within(() => {
cy.findByText(/no team/i).should("exist");
cy.findByText(/apples/i).should("exist");
cy.findByText(/oranges/i).should("exist");

View File

@ -137,16 +137,13 @@ describe("Basic tier - Team observer/maintainer user", () => {
cy.findByText(/add new host/i).click();
// See the “Select a team for this new host” in the Add new host modal. This modal appears after the user selects the “Add new host” button
cy.get(".add-host-modal__team-dropdown-wrapper").within(() => {
cy.findByText(/select a team for this new host/i).should("exist");
cy.get(".Select").within(() => {
cy.findByText(/select a team/i).click();
cy.findByText(/no team/i).should("exist");
// cy.findByText(/apples/i).should("exist");
// cy.findByText(/oranges/i).should("not exist");
// ^ TODO: Team maintainer has access to only their teams, team observer does not have access
});
cy.get(".add-host-modal__team-dropdown-wrapper .Select-control").click();
cy.get(".Select-menu-outer").within(() => {
cy.findByText(/no team/i).should("not.exist");
cy.findByText(/apples/i).should("not.exist");
cy.findByText(/oranges/i).should("exist");
});
cy.findByRole("button", { name: /done/i }).click();
// On the Host details page, they should…

View File

@ -17,7 +17,6 @@ import teamInterface from "interfaces/team";
import userInterface from "interfaces/user";
import osqueryTableInterface from "interfaces/osquery_table";
import statusLabelsInterface from "interfaces/status_labels";
import enrollSecretInterface from "interfaces/enroll_secret";
import { selectOsqueryTable } from "redux/nodes/components/QueryPages/actions";
import { renderFlash } from "redux/nodes/notifications/actions";
import labelActions from "redux/nodes/entities/labels/actions";
@ -103,7 +102,6 @@ export class ManageHostsPage extends PureComponent {
routeParams: PropTypes.objectOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
),
enrollSecret: enrollSecretInterface,
selectedFilters: PropTypes.arrayOf(PropTypes.string),
selectedLabel: labelInterface,
selectedTeam: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
@ -223,12 +221,6 @@ export class ManageHostsPage extends PureComponent {
toggleAddHostModal();
};
// The onChange method below is for the dropdown used in modals
onChangeTeam = (team) => {
const { dispatch } = this.props;
dispatch(teamActions.getEnrollSecrets(team));
};
// NOTE: this is called once on the initial rendering. The initial render of
// the TableContainer child component will call this handler.
onTableQueryChange = async ({
@ -707,7 +699,7 @@ export class ManageHostsPage extends PureComponent {
renderAddHostModal = () => {
const { toggleAddHostModal, onChangeTeam } = this;
const { showAddHostModal } = this.state;
const { enrollSecret, config, canAddNewHosts, teams } = this.props;
const { config, currentUser, canAddNewHosts, teams } = this.props;
if (!canAddNewHosts || !showAddHostModal) {
return null;
@ -723,8 +715,8 @@ export class ManageHostsPage extends PureComponent {
teams={teams}
onChangeTeam={onChangeTeam}
onReturnToApp={toggleAddHostModal}
enrollSecret={enrollSecret}
config={config}
currentUser={currentUser}
/>
</Modal>
);
@ -1056,7 +1048,6 @@ const mapStateToProps = (state, ownProps) => {
const { selectedOsqueryTable } = state.components.QueryPages;
const { errors: labelErrors, loading: loadingLabels } = state.entities.labels;
const enrollSecret = state.app.enrollSecret;
const config = state.app.config;
const { loading: loadingHosts } = state.entities.hosts;
@ -1090,7 +1081,6 @@ const mapStateToProps = (state, ownProps) => {
labelErrors,
labels,
loadingLabels,
enrollSecret,
selectedLabel,
selectedOsqueryTable,
statusLabels,

View File

@ -6,7 +6,7 @@ import Fleet from "fleet";
import Button from "components/buttons/Button";
import configInterface from "interfaces/config";
import teamInterface from "interfaces/team";
import enrollSecretInterface from "interfaces/enroll_secret";
import userInterface from "interfaces/user";
import permissionUtils from "utilities/permissions";
import EnrollSecretTable from "components/config/EnrollSecretTable";
import FleetIcon from "components/icons/FleetIcon";
@ -23,18 +23,62 @@ const NO_TEAM_OPTION = {
class AddHostModal extends Component {
static propTypes = {
teams: PropTypes.arrayOf(teamInterface),
onChangeTeam: PropTypes.func,
onReturnToApp: PropTypes.func,
enrollSecret: enrollSecretInterface,
config: configInterface,
currentUser: userInterface,
};
constructor(props) {
super(props);
this.state = { fetchCertificateError: undefined, selectedTeam: null };
this.userRole = {
isAnyTeamMaintainer: permissionUtils.isAnyTeamMaintainer(
this.props.currentUser
),
isGlobalAdmin: permissionUtils.isGlobalAdmin(this.props.currentUser),
isGlobalMaintainer: permissionUtils.isGlobalMaintainer(
this.props.currentUser
),
};
this.currentUserTeams = this.userRole.isAnyTeamMaintainer
? Object.values(this.props.currentUser.teams).filter(
(team) => team.role === "maintainer"
)
: this.props.teams;
this.teamSecrets = Object.values(this.props.teams).map((team) => {
return { id: team.id, name: team.name, secrets: team.secrets };
});
this.state = {
fetchCertificateError: undefined,
selectedTeam: null,
globalSecrets: [],
selectedEnrollSecrets: [],
};
}
componentDidMount() {
const { isGlobalAdmin, isGlobalMaintainer } = this.userRole;
(() => {
if (isGlobalAdmin || isGlobalMaintainer) {
Fleet.config
.loadEnrollSecret()
.then((response) => {
this.setState({
globalSecrets: response.spec.secrets,
selectedTeam: { id: NO_TEAM_OPTION.value }, // Reset initial selectedTeam value to "no-team" in the case of global users
});
})
.catch((err) => {
console.log(err);
});
} else {
this.setState({ selectedTeam: this.currentUserTeams[0] });
}
})();
Fleet.config
.loadCertificate()
.then((certificate) => {
@ -63,32 +107,57 @@ class AddHostModal extends Component {
return false;
};
// if isGlobalAdmin or isGlobalMaintainer, we include a "No team" option and reveal globalSecrets
// if not, we pull secrets for the user's teams from the teamsSecrets
onChangeSelectTeam = (teamId) => {
const { teams, onChangeTeam } = this.props;
const { globalSecrets } = this.state;
const { currentUserTeams, teamSecrets } = this;
if (teamId === "no-team") {
onChangeTeam(null);
this.setState({ selectedTeam: { id: NO_TEAM_OPTION.value } });
this.setState({ selectedEnrollSecrets: globalSecrets || [] });
} else {
const selectedTeam = teams.find((team) => team.id === teamId);
onChangeTeam(selectedTeam);
const selectedTeam = currentUserTeams.find((team) => team.id === teamId);
const selectedEnrollSecrets =
teamSecrets.find((e) => e.id === selectedTeam.id)?.secrets || "";
this.setState({ selectedTeam });
this.setState({
selectedEnrollSecrets,
});
}
};
createTeamDropdownOptions = (teams) => {
const teamOptions = teams.map((team) => {
getSelectedEnrollSecrets = (selectedTeam) => {
if (selectedTeam.id === NO_TEAM_OPTION.value) {
return this.state.globalSecrets;
}
return (
this.teamSecrets.find((e) => e.id === selectedTeam.id)?.secrets || ""
);
};
createTeamDropdownOptions = (currentUserTeams) => {
const teamOptions = currentUserTeams.map((team) => {
return {
value: team.id,
label: team.name,
};
});
return [NO_TEAM_OPTION, ...teamOptions];
return this.userRole.isAnyTeamMaintainer
? teamOptions
: [NO_TEAM_OPTION, ...teamOptions];
};
render() {
const { config, onReturnToApp, enrollSecret, teams } = this.props;
const { fetchCertificateError, selectedTeam } = this.state;
const { createTeamDropdownOptions, onChangeSelectTeam } = this;
const { config, onReturnToApp } = this.props;
const { fetchCertificateError, selectedTeam, globalSecrets } = this.state;
const {
createTeamDropdownOptions,
currentUserTeams,
getSelectedEnrollSecrets,
onChangeSelectTeam,
} = this;
const isBasicTier = permissionUtils.isBasicTier(config);
let tlsHostname = config.server_url;
try {
@ -172,18 +241,23 @@ class AddHostModal extends Component {
server.
</p>
<div className={`${baseClass}__secret-wrapper`}>
{permissionUtils.isBasicTier(config) ? (
{isBasicTier ? (
<Dropdown
wrapperClassName={`${baseClass}__team-dropdown-wrapper`}
label={"Select a team for this new host:"}
value={selectedTeam && selectedTeam.id}
options={createTeamDropdownOptions(teams)}
options={createTeamDropdownOptions(currentUserTeams)}
onChange={onChangeSelectTeam}
placeholder={"Select a team"}
searchable={false}
/>
) : null}
<EnrollSecretTable secrets={enrollSecret} />
{isBasicTier && selectedTeam && (
<EnrollSecretTable
secrets={getSelectedEnrollSecrets(selectedTeam)}
/>
)}
{!isBasicTier && <EnrollSecretTable secrets={globalSecrets} />}
</div>
</li>
<li>