Reinstate generate installer code from #2493 (#2771)

This commit is contained in:
RachelElysia
2021-11-09 15:02:01 -05:00
committed by GitHub
parent 2e7c092e1e
commit 2d1e4c0898
15 changed files with 80 additions and 630 deletions
@@ -0,0 +1 @@
* Introduce Generate installer modal to replace Add new host modal for better UX
+1 -1
View File
@@ -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/)
+7 -9
View File
@@ -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");
});
+4 -4
View File
@@ -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"
+3 -4
View File
@@ -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();
+2 -2
View File
@@ -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(() => {
+3 -13
View File
@@ -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();
+2 -2
View File
@@ -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>