@@ -0,0 +1 @@
|
||||
* Introduce Generate installer modal to replace Add new host modal for better UX
|
||||
+1
-1
@@ -39,4 +39,4 @@ As much as possible, assert that the code is only selecting 1 item or that the f
|
||||
|
||||
- [Fleet testing documentation](../docs/03-Contributing/02-Testing.md)
|
||||
- [Cypress documentation](https://docs.cypress.io/api/table-of-contents)
|
||||
- [React testing-library query documentation](https://testing-library.com/docs/queries/about)
|
||||
- [React testing-library query documentation](https://testing-library.com/docs/queries/about/)
|
||||
|
||||
@@ -28,10 +28,10 @@ describe(
|
||||
() => {
|
||||
cy.visit("/hosts/manage");
|
||||
|
||||
cy.contains("button", /add new host/i).click();
|
||||
cy.contains("button", /generate installer/i).click();
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(1000);
|
||||
cy.get('a[href*="showSecret"]').click();
|
||||
cy.wait(2000);
|
||||
cy.findByText(/rpm/i).click();
|
||||
cy.contains("a", /download/i)
|
||||
.first()
|
||||
.click();
|
||||
@@ -42,13 +42,11 @@ describe(
|
||||
if (Cypress.platform !== "win32") {
|
||||
// windows has issues with downloads location
|
||||
cy.readFile(
|
||||
path.join(Cypress.config("downloadsFolder"), "secret.txt"),
|
||||
path.join(Cypress.config("downloadsFolder"), "fleet.pem"),
|
||||
{
|
||||
timeout: 5000,
|
||||
}
|
||||
).then((contents) => {
|
||||
cy.get("input[disabled]").should("have.value", contents);
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
cy.visit("/hosts/manage");
|
||||
@@ -141,8 +139,8 @@ describe(
|
||||
cy.findByRole("button", { name: /delete/i }).click();
|
||||
})
|
||||
.then(() => {
|
||||
cy.findByText(/add your hosts to fleet/i).should("exist");
|
||||
cy.findByText(/add new host/i).should("exist");
|
||||
cy.findByText(/add your devices to fleet/i).should("exist");
|
||||
cy.findByText(/generate installer/i).should("exist");
|
||||
cy.findByText(/about this host/i).should("not.exist");
|
||||
cy.findByText(hostname).should("not.exist");
|
||||
});
|
||||
|
||||
@@ -37,13 +37,13 @@ describe(
|
||||
cy.findByText(/settings/i).should("exist");
|
||||
});
|
||||
|
||||
// See and select "add new host"
|
||||
cy.findByRole("button", { name: /add new host/i }).click();
|
||||
// See and select "generate installer"
|
||||
cy.findByRole("button", { name: /generate installer/i }).click();
|
||||
cy.contains(/team/i).should("not.exist");
|
||||
cy.contains("button", /done/i).click();
|
||||
|
||||
// See the “Show enroll secret” button. A modal appears after the user selects the button
|
||||
cy.contains("button", /show enroll secret/i).click();
|
||||
// See the "Manage" enroll secret” button. A modal appears after the user selects the button
|
||||
cy.contains("button", /manage enroll secret/i).click();
|
||||
cy.contains("button", /done/i).click();
|
||||
|
||||
// See and select "add label"
|
||||
|
||||
@@ -36,12 +36,11 @@ describe(
|
||||
|
||||
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||
cy.findByText(/teams/i).should("not.exist");
|
||||
cy.contains("button", /add new host/i).click();
|
||||
cy.findByText("select a team").should("not.exist");
|
||||
cy.contains("button", /generate installer/i).click();
|
||||
cy.contains("button", /done/i).click();
|
||||
|
||||
// See the “Show enroll secret” button. A modal appears after the user selects the button
|
||||
cy.contains("button", /show enroll secret/i).click();
|
||||
// See the Manage enroll secret” button. A modal appears after the user selects the button
|
||||
cy.contains("button", /manage enroll secret/i).click();
|
||||
cy.contains("button", /done/i).click();
|
||||
|
||||
cy.contains("button", /add label/i).click();
|
||||
|
||||
@@ -39,9 +39,9 @@ describe("Free tier - Observer user", () => {
|
||||
// Host manage page: No team UI, cannot add host, add label, nor enroll secret
|
||||
cy.visit("/hosts/manage");
|
||||
cy.findByText(/teams/i).should("not.exist");
|
||||
cy.contains("button", /add new host/i).should("not.exist");
|
||||
cy.contains("button", /generate installer/i).should("not.exist");
|
||||
cy.contains("button", /add label/i).should("not.exist");
|
||||
cy.contains("button", /show enroll secret/i).should("not.exist");
|
||||
cy.contains("button", /manage enroll secret/i).should("not.exist");
|
||||
|
||||
// Host details page: No team UI, cannot delete or query
|
||||
cy.get("tbody").within(() => {
|
||||
|
||||
@@ -36,19 +36,9 @@ describe(
|
||||
// See the “Teams” column in the Hosts table
|
||||
cy.get("thead").contains(/team/i).should("exist");
|
||||
|
||||
// See and select the “Add new host” button
|
||||
cy.contains("button", /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 .Select-control"
|
||||
).click();
|
||||
|
||||
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");
|
||||
});
|
||||
// See and select the “Generate installer” button
|
||||
cy.contains("button", /generate installer/i).click();
|
||||
cy.contains("button", /done/i).click();
|
||||
|
||||
// On the Host details page, they should…
|
||||
// See the “Team” information below the hostname
|
||||
|
||||
@@ -29,9 +29,9 @@ describe(
|
||||
|
||||
cy.wait(3000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||
|
||||
cy.findByText(/show enroll secret/i).should("exist");
|
||||
cy.findByText(/manage enroll secret/i).should("exist");
|
||||
|
||||
cy.contains("button", /add new host/i).click();
|
||||
cy.contains("button", /generate installer/i).click();
|
||||
// TODO: Check Team Apples is in Select a team dropdown
|
||||
cy.contains("button", /done/i).click();
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@ describe("Premium tier - Observer user", () => {
|
||||
cy.contains("button", /query/i).click();
|
||||
cy.contains("button", /create custom query/i).should("not.exist");
|
||||
|
||||
// Not see the "Show enroll secret” button
|
||||
cy.contains("button", /show enroll secret/i).should("not.exist");
|
||||
// Not see the "Manage enroll secret” button
|
||||
cy.contains("button", /manage enroll secret/i).should("not.exist");
|
||||
|
||||
// TODO - Fix tests according to improved query experience - MP
|
||||
// Query pages: Can see team in select targets dropdown
|
||||
|
||||
@@ -27,7 +27,7 @@ describe(
|
||||
// On the Hosts page, they should…
|
||||
|
||||
// See hosts
|
||||
// cy.findByText(/kinda empty in here/i).should("not.exist");
|
||||
// cy.findByText(/generate installer/i).should("not.exist");
|
||||
// ^^TODO hosts table is not rendering because we need new forEach script/command for admin to assign team after the host is added
|
||||
|
||||
// See the “Teams” column in the Hosts table
|
||||
@@ -140,16 +140,8 @@ describe(
|
||||
// cy.get("thead").contains(/team/i).should("exist");
|
||||
// ^^TODO hosts table is not rendering because we need new forEach script/command for admin to assign team after the host is added
|
||||
|
||||
// See and select the “Add new host” button
|
||||
cy.findByRole("button", { name: /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 .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");
|
||||
});
|
||||
// See and select the “Generate installer” button
|
||||
cy.findByRole("button", { name: /generate installer/i }).click();
|
||||
cy.findByRole("button", { name: /done/i }).click();
|
||||
|
||||
// On the Host details page, they should…
|
||||
|
||||
@@ -73,7 +73,7 @@ import EditColumnsModal from "./components/EditColumnsModal/EditColumnsModal";
|
||||
import TransferHostModal from "./components/TransferHostModal";
|
||||
import DeleteHostModal from "./components/DeleteHostModal";
|
||||
import SoftwareVulnerabilities from "./components/SoftwareVulnerabilities"; // @ts-ignore
|
||||
import AddHostModal from "./components/AddHostModal";
|
||||
import GenerateInstallerModal from "./components/GenerateInstallerModal";
|
||||
import EditColumnsIcon from "../../../../assets/images/icon-edit-columns-16x16@2x.png";
|
||||
import PencilIcon from "../../../../assets/images/icon-pencil-14x14@2x.png";
|
||||
import TrashIcon from "../../../../assets/images/icon-trash-14x14@2x.png";
|
||||
@@ -157,7 +157,6 @@ const ManageHostsPage = ({
|
||||
// ========= states
|
||||
const [selectedLabel, setSelectedLabel] = useState<ILabel>();
|
||||
const [statusLabels, setStatusLabels] = useState<IStatusLabels>();
|
||||
const [showAddHostModal, setShowAddHostModal] = useState<boolean>(false);
|
||||
const [showEnrollSecretModal, setShowEnrollSecretModal] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
@@ -167,6 +166,10 @@ const ManageHostsPage = ({
|
||||
const [showEditColumnsModal, setShowEditColumnsModal] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [
|
||||
showGenerateInstallerModal,
|
||||
setShowGenerateInstallerModal,
|
||||
] = useState<boolean>(false);
|
||||
const [showTransferHostModal, setShowTransferHostModal] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
@@ -220,6 +223,11 @@ const ManageHostsPage = ({
|
||||
isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer;
|
||||
const canAddNewLabels = isGlobalAdmin || isGlobalMaintainer;
|
||||
|
||||
const generateInstallerTeam = currentTeam || {
|
||||
name: "No team",
|
||||
secrets: globalSecret,
|
||||
};
|
||||
|
||||
const {
|
||||
isLoading: isLabelsLoading,
|
||||
data: labels,
|
||||
@@ -282,6 +290,10 @@ const ManageHostsPage = ({
|
||||
setShowDeleteHostModal(!showDeleteHostModal);
|
||||
};
|
||||
|
||||
const toggleGenerateInstallerModal = () => {
|
||||
setShowGenerateInstallerModal(!showGenerateInstallerModal);
|
||||
};
|
||||
|
||||
const toggleAllMatchingHosts = (shouldSelect: boolean) => {
|
||||
if (typeof shouldSelect !== "undefined") {
|
||||
setIsAllMatchingHostsSelected(shouldSelect);
|
||||
@@ -290,27 +302,6 @@ const ManageHostsPage = ({
|
||||
}
|
||||
};
|
||||
|
||||
const renderAddHostModal = () => {
|
||||
if (!canAddNewHosts || !showAddHostModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="New host"
|
||||
onExit={() => setShowAddHostModal(false)}
|
||||
className={`${baseClass}__invite-modal`}
|
||||
>
|
||||
<AddHostModal
|
||||
teams={teams}
|
||||
onReturnToApp={() => setShowAddHostModal(false)}
|
||||
config={config}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const getLabelSelected = () => {
|
||||
return selectedFilters.find((f) => f.includes(LABEL_SLUG_PREFIX));
|
||||
};
|
||||
@@ -1041,6 +1032,19 @@ const ManageHostsPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderGenerateInstallerModal = () => {
|
||||
if (!showGenerateInstallerModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GenerateInstallerModal
|
||||
onCancel={toggleGenerateInstallerModal}
|
||||
selectedTeam={generateInstallerTeam}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTransferHostModal = () => {
|
||||
if (!showTransferHostModal || !teams) {
|
||||
return null;
|
||||
@@ -1221,7 +1225,7 @@ const ManageHostsPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderTable = () => {
|
||||
const renderTable = (selectedTeam: number) => {
|
||||
if (
|
||||
!config ||
|
||||
!currentUser ||
|
||||
@@ -1237,8 +1241,15 @@ const ManageHostsPage = ({
|
||||
}
|
||||
|
||||
// Hosts have not been set up for this instance yet.
|
||||
if (getStatusSelected() === ALL_HOSTS_LABEL && selectedLabel.count === 0) {
|
||||
return <NoHosts setShowAddHostModal={setShowAddHostModal} />;
|
||||
if (
|
||||
(getStatusSelected() === ALL_HOSTS_LABEL && selectedLabel.count === 0) ||
|
||||
(getStatusSelected() === ALL_HOSTS_LABEL &&
|
||||
filteredHostCount === 0 &&
|
||||
searchQuery === "")
|
||||
) {
|
||||
return (
|
||||
<NoHosts toggleGenerateInstallerModal={toggleGenerateInstallerModal} />
|
||||
);
|
||||
}
|
||||
|
||||
const secondarySelectActions: IActionButtonProps[] = [
|
||||
@@ -1294,6 +1305,8 @@ const ManageHostsPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const selectedTeam = currentTeam?.id || 0;
|
||||
|
||||
return (
|
||||
<div className="has-sidebar">
|
||||
{renderForm()}
|
||||
@@ -1308,7 +1321,7 @@ const ManageHostsPage = ({
|
||||
className={`${baseClass}__enroll-hosts button`}
|
||||
variant="inverse"
|
||||
>
|
||||
<span>Show enroll secret</span>
|
||||
<span>Manage enroll secret</span>
|
||||
</Button>
|
||||
)}
|
||||
{canAddNewHosts &&
|
||||
@@ -1321,24 +1334,24 @@ const ManageHostsPage = ({
|
||||
filteredHostCount === 0
|
||||
) && (
|
||||
<Button
|
||||
onClick={() => setShowAddHostModal(true)}
|
||||
onClick={toggleGenerateInstallerModal}
|
||||
className={`${baseClass}__add-hosts button button--brand`}
|
||||
>
|
||||
<span>Add new host</span>
|
||||
<span>Generate installer</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{renderActiveFilterBlock()}
|
||||
{renderSoftwareVulnerabilities()}
|
||||
{config && (!isPremiumTier || teams) && renderTable()}
|
||||
{config && (!isPremiumTier || teams) && renderTable(selectedTeam)}
|
||||
</div>
|
||||
)}
|
||||
{!isLabelsLoading && renderSidePanel()}
|
||||
{renderAddHostModal()}
|
||||
{renderEnrollSecretModal()}
|
||||
{renderEditColumnsModal()}
|
||||
{renderDeleteLabelModal()}
|
||||
{renderGenerateInstallerModal()}
|
||||
{renderTransferHostModal()}
|
||||
{renderDeleteHostModal()}
|
||||
</div>
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
import Fleet from "fleet";
|
||||
import Button from "components/buttons/Button";
|
||||
import configInterface from "interfaces/config";
|
||||
import teamInterface from "interfaces/team";
|
||||
import userInterface from "interfaces/user";
|
||||
import permissionUtils from "utilities/permissions";
|
||||
import EnrollSecretTable from "components/EnrollSecretTable";
|
||||
import FleetIcon from "components/icons/FleetIcon";
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import { stringToClipboard } from "utilities/copy_text";
|
||||
import DownloadIcon from "../../../../../../assets/images/icon-download-12x12@2x.png";
|
||||
|
||||
const baseClass = "add-host-modal";
|
||||
|
||||
const NO_TEAM_OPTION = {
|
||||
value: "no-team",
|
||||
label: "No team",
|
||||
};
|
||||
|
||||
class AddHostModal extends Component {
|
||||
static propTypes = {
|
||||
teams: PropTypes.arrayOf(teamInterface),
|
||||
onReturnToApp: PropTypes.func,
|
||||
config: configInterface,
|
||||
currentUser: userInterface,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
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 = this.props.teams
|
||||
? 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: [],
|
||||
copyMessage: "",
|
||||
};
|
||||
}
|
||||
|
||||
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) => {
|
||||
this.setState({ certificate });
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
fetchCertificateError:
|
||||
"Failed to load certificate. Is Fleet app URL configured properly?",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onFetchCertificate = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { certificate } = this.state;
|
||||
|
||||
const filename = "fleet.pem";
|
||||
const file = new global.window.File([certificate], filename, {
|
||||
type: "application/x-pem-file",
|
||||
});
|
||||
|
||||
FileSaver.saveAs(file);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
onCopyRunCommand = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
stringToClipboard("osqueryd --flagfile=flagfile.txt --verbose")
|
||||
.then(() => this.setState({ copyMessage: "Copied!" }))
|
||||
.catch(() => this.setState({ copyMessage: "Copy failed" }));
|
||||
|
||||
// Clear message after 1 second
|
||||
setTimeout(() => this.setState({ copyMessage: "" }), 1000);
|
||||
|
||||
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 { globalSecrets } = this.state;
|
||||
const { currentUserTeams, teamSecrets } = this;
|
||||
if (teamId === "no-team") {
|
||||
this.setState({ selectedTeam: { id: NO_TEAM_OPTION.value } });
|
||||
this.setState({ selectedEnrollSecrets: globalSecrets || [] });
|
||||
} else {
|
||||
const selectedTeam = currentUserTeams.find((team) => team.id === teamId);
|
||||
const selectedEnrollSecrets =
|
||||
teamSecrets.find((e) => e.id === selectedTeam.id)?.secrets || "";
|
||||
this.setState({ selectedTeam });
|
||||
this.setState({
|
||||
selectedEnrollSecrets,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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 this.userRole.isAnyTeamMaintainer
|
||||
? teamOptions
|
||||
: [NO_TEAM_OPTION, ...teamOptions];
|
||||
};
|
||||
|
||||
renderRunCommandLabel = () => {
|
||||
const { copyMessage } = this.state;
|
||||
const { onCopyRunCommand } = this;
|
||||
|
||||
return (
|
||||
<span className={`${baseClass}__name`}>
|
||||
<span className="buttons">
|
||||
{copyMessage && <span>{`${copyMessage} `}</span>}
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className={`${baseClass}__run-osquery-copy-icon`}
|
||||
onClick={onCopyRunCommand}
|
||||
>
|
||||
<FleetIcon name="clipboard" />
|
||||
</Button>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { config, onReturnToApp } = this.props;
|
||||
const { fetchCertificateError, selectedTeam, globalSecrets } = this.state;
|
||||
const {
|
||||
createTeamDropdownOptions,
|
||||
currentUserTeams,
|
||||
getSelectedEnrollSecrets,
|
||||
onChangeSelectTeam,
|
||||
renderRunCommandLabel,
|
||||
} = this;
|
||||
|
||||
const isPremiumTier = permissionUtils.isPremiumTier(config);
|
||||
|
||||
let tlsHostname = config.server_url;
|
||||
try {
|
||||
const serverUrl = new URL(config.server_url);
|
||||
tlsHostname = serverUrl.hostname;
|
||||
if (serverUrl.port) {
|
||||
tlsHostname += `:${serverUrl.port}`;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof TypeError)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const flagfileContent = `# Server
|
||||
--tls_hostname=${tlsHostname}
|
||||
--tls_server_certs=fleet.pem
|
||||
# Enrollment
|
||||
--host_identifier=instance
|
||||
--enroll_secret_path=secret.txt
|
||||
--enroll_tls_endpoint=/api/v1/osquery/enroll
|
||||
# Configuration
|
||||
--config_plugin=tls
|
||||
--config_tls_endpoint=/api/v1/osquery/config
|
||||
--config_refresh=10
|
||||
# Live query
|
||||
--disable_distributed=false
|
||||
--distributed_plugin=tls
|
||||
--distributed_interval=10
|
||||
--distributed_tls_max_attempts=3
|
||||
--distributed_tls_read_endpoint=/api/v1/osquery/distributed/read
|
||||
--distributed_tls_write_endpoint=/api/v1/osquery/distributed/write
|
||||
# Logging
|
||||
--logger_plugin=tls
|
||||
--logger_tls_endpoint=/api/v1/osquery/log
|
||||
--logger_tls_period=10
|
||||
# File carving
|
||||
--disable_carver=false
|
||||
--carver_start_endpoint=/api/v1/osquery/carve/begin
|
||||
--carver_continue_endpoint=/api/v1/osquery/carve/block
|
||||
--carver_block_size=2000000`;
|
||||
|
||||
const onDownloadFlagfile = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const filename = "flagfile.txt";
|
||||
const file = new global.window.File([flagfileContent], filename);
|
||||
|
||||
FileSaver.saveAs(file);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__manual-install-content`}>
|
||||
<div className={`${baseClass}__documentation-link`}>
|
||||
<h4>
|
||||
<a
|
||||
href="https://github.com/fleetdm/fleet/blob/2f42c281f98e39a72ab4a5125ecd26d303a16a6b/docs/1-Using-Fleet/4-Adding-hosts.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Add Hosts Documentation <FleetIcon name="external-link" />
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<ol className={`${baseClass}__install-steps`}>
|
||||
<li>
|
||||
<h4>
|
||||
<span className={`${baseClass}__step-number`}>1</span>Enroll
|
||||
secret
|
||||
</h4>
|
||||
<p>
|
||||
Osquery uses an enroll secret to authenticate with the Fleet
|
||||
server.
|
||||
</p>
|
||||
<div className={`${baseClass}__secret-wrapper`}>
|
||||
{isPremiumTier && currentUserTeams ? (
|
||||
<Dropdown
|
||||
wrapperClassName={`${baseClass}__team-dropdown-wrapper`}
|
||||
label={"Select a team for this new host:"}
|
||||
value={selectedTeam && selectedTeam.id}
|
||||
options={createTeamDropdownOptions(currentUserTeams)}
|
||||
onChange={onChangeSelectTeam}
|
||||
placeholder={"Select a team"}
|
||||
searchable={false}
|
||||
/>
|
||||
) : null}
|
||||
{isPremiumTier && selectedTeam && (
|
||||
<EnrollSecretTable
|
||||
secrets={getSelectedEnrollSecrets(selectedTeam)}
|
||||
/>
|
||||
)}
|
||||
{!isPremiumTier && (
|
||||
<EnrollSecretTable secrets={globalSecrets} />
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<h4>
|
||||
<span className={`${baseClass}__step-number`}>2</span>Server
|
||||
certificate
|
||||
</h4>
|
||||
<p>
|
||||
Provide the TLS certificate used by the Fleet server to enable
|
||||
secure connections from osquery:
|
||||
</p>
|
||||
<p>
|
||||
{fetchCertificateError ? (
|
||||
<span className={`${baseClass}__error`}>
|
||||
{fetchCertificateError}
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
href="#downloadCertificate"
|
||||
onClick={this.onFetchCertificate}
|
||||
>
|
||||
Download
|
||||
<img src={DownloadIcon} alt="download icon" />
|
||||
</a>
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>
|
||||
<span className={`${baseClass}__step-number`}>3</span>Flagfile
|
||||
</h4>
|
||||
<p>
|
||||
If using the enroll secret and server certificate downloaded
|
||||
above, use the generated flagfile. In some configurations,
|
||||
modifications may need to be made:
|
||||
</p>
|
||||
<p>
|
||||
<a href="#downloadFlagfile" onClick={onDownloadFlagfile}>
|
||||
Download
|
||||
<img src={DownloadIcon} alt="download icon" />
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>
|
||||
<span className={`${baseClass}__step-number`}>4</span>Run
|
||||
osquery
|
||||
</h4>
|
||||
<p>
|
||||
Run osquery from the directory containing the above files (may
|
||||
require sudo or Run as Administrator privileges):
|
||||
</p>
|
||||
<div>
|
||||
<InputField
|
||||
disabled
|
||||
inputWrapperClass={`${baseClass}__run-osquery-input`}
|
||||
name="run-osquery"
|
||||
label={renderRunCommandLabel()}
|
||||
type={"text"}
|
||||
value={"osqueryd --flagfile=flagfile.txt --verbose"}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className={`${baseClass}__button-wrap`}>
|
||||
<Button onClick={onReturnToApp} className="button button--brand">
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AddHostModal;
|
||||
@@ -1,171 +0,0 @@
|
||||
.add-host-modal {
|
||||
&__manual-install-header {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: $bold;
|
||||
line-height: 1.33;
|
||||
letter-spacing: -0.6px;
|
||||
color: #48c586;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-weight: $regular;
|
||||
line-height: 1.6;
|
||||
letter-spacing: normal;
|
||||
color: rgba(32, 37, 50, 0.48);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.fleeticon {
|
||||
float: left;
|
||||
font-size: 44px;
|
||||
color: #48c586;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&__manual-install-content {
|
||||
h4 {
|
||||
font-size: $small;
|
||||
font-weight: $bold;
|
||||
margin: $pad-xlarge 0 0;
|
||||
|
||||
.fleeticon {
|
||||
margin-left: 5px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $x-small;
|
||||
font-weight: $regular;
|
||||
line-height: 20px;
|
||||
letter-spacing: normal;
|
||||
color: $core-fleet-black;
|
||||
margin: 8px 0 0 44px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: $x-small;
|
||||
color: $core-vibrant-blue;
|
||||
font-weight: $bold;
|
||||
text-decoration: none;
|
||||
|
||||
img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__documentation-link {
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__install-steps {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
&__step-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 100px;
|
||||
border: 2px solid $core-vibrant-blue;
|
||||
color: $core-vibrant-blue;
|
||||
font-weight: $bold;
|
||||
text-align: center;
|
||||
margin-right: $pad-medium;
|
||||
}
|
||||
|
||||
&__button-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: $pad-large 0 0;
|
||||
}
|
||||
|
||||
&__download-cert {
|
||||
text-align: center;
|
||||
padding-top: 15px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&__reveal-secret {
|
||||
float: right;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&__secret-wrapper {
|
||||
position: relative;
|
||||
margin: $pad-medium 0 0 44px;
|
||||
}
|
||||
|
||||
&__team-dropdown-wrapper {
|
||||
.form-field__label {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__team-dropdown-wrapper {
|
||||
.form-field__label {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
background-color: $ui-off-white;
|
||||
color: $core-fleet-blue;
|
||||
border: 1px solid $ui-fleet-blue-15;
|
||||
border-radius: 4px;
|
||||
padding: 7px $pad-medium;
|
||||
margin: $pad-large 0 0 44px;
|
||||
}
|
||||
|
||||
&__error {
|
||||
color: $ui-error;
|
||||
}
|
||||
|
||||
&__run-osquery-input {
|
||||
margin-left: 44px;
|
||||
}
|
||||
|
||||
&__run-osquery-copy-icon {
|
||||
color: $core-vibrant-blue;
|
||||
margin-left: $pad-small;
|
||||
margin-right: $pad-xxlarge;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
transform: translateY(80%);
|
||||
height: 16px;
|
||||
|
||||
span {
|
||||
font-weight: $regular;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./AddHostModal";
|
||||
@@ -8,26 +8,28 @@ import { ITeam } from "interfaces/team";
|
||||
import RoboDogImage from "../../../../../../assets/images/robo-dog-176x144@2x.png";
|
||||
|
||||
interface INoHostsProps {
|
||||
setShowAddHostModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
toggleGenerateInstallerModal: () => void;
|
||||
}
|
||||
|
||||
const baseClass = "no-hosts";
|
||||
|
||||
const NoHosts = ({ setShowAddHostModal }: INoHostsProps): JSX.Element => {
|
||||
const NoHosts = ({
|
||||
toggleGenerateInstallerModal,
|
||||
}: INoHostsProps): JSX.Element => {
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<div className={`${baseClass}__inner`}>
|
||||
<img src={RoboDogImage} alt="No Hosts" />
|
||||
<div>
|
||||
<h2>Add your hosts to Fleet</h2>
|
||||
<p>Add your laptops and servers to securely monitor them.</p>
|
||||
<h2>Add your devices to Fleet</h2>
|
||||
<p>Generate an installer to add your own devices.</p>
|
||||
<div className={`${baseClass}__no-hosts-button`}>
|
||||
<Button
|
||||
onClick={() => setShowAddHostModal(true)}
|
||||
onClick={toggleGenerateInstallerModal}
|
||||
type="button"
|
||||
className="button button--brand"
|
||||
>
|
||||
Add new hosts
|
||||
Generate installer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user