diff --git a/assets/images/icon-copy-clipboard-fleet-blue-20x20@2x.png b/assets/images/icon-copy-clipboard-fleet-blue-20x20@2x.png new file mode 100644 index 0000000000..467bca2c54 Binary files /dev/null and b/assets/images/icon-copy-clipboard-fleet-blue-20x20@2x.png differ diff --git a/changes/issue-2290-generate-installer-new-to-fleet b/changes/issue-2290-generate-installer-new-to-fleet new file mode 100644 index 0000000000..6d33331615 --- /dev/null +++ b/changes/issue-2290-generate-installer-new-to-fleet @@ -0,0 +1 @@ +* New CTA for new users to generate installer to add hosts onto Fleet \ No newline at end of file diff --git a/cypress/integration/all/app/hosts.spec.ts b/cypress/integration/all/app/hosts.spec.ts index 0a38951752..35b48e2cbb 100644 --- a/cypress/integration/all/app/hosts.spec.ts +++ b/cypress/integration/all/app/hosts.spec.ts @@ -27,9 +27,10 @@ describe( () => { cy.visit("/hosts/manage"); - cy.contains("button", /add new host/i).click(); - - cy.get('a[href*="showSecret"]').click(); + cy.contains("button", /generate installer/i).click(); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(2000); + cy.findByText(/rpm/i).click(); cy.contains("a", /download/i) .first() .click(); @@ -40,13 +41,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"); @@ -104,7 +103,7 @@ describe( // }) // .then(() => { // cy.findByText(/successfully deleted/i).should("exist"); - // cy.findByText(/kinda empty in here/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"); // }); diff --git a/cypress/integration/free/admin.spec.ts b/cypress/integration/free/admin.spec.ts index d232e7fe4a..2951832f96 100644 --- a/cypress/integration/free/admin.spec.ts +++ b/cypress/integration/free/admin.spec.ts @@ -37,13 +37,12 @@ describe( cy.findByText(/settings/i).should("exist"); }); - // See and select "add new host" - cy.findByRole("button", { name: /add new host/i }).click(); - cy.contains(/team/i).should("not.exist"); + // See and select "generate installer" + cy.findByRole("button", { name: /generate installer/i }).click(); cy.findByRole("button", { name: /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" diff --git a/cypress/integration/free/maintainer.spec.ts b/cypress/integration/free/maintainer.spec.ts index 845430ee40..224329aa08 100644 --- a/cypress/integration/free/maintainer.spec.ts +++ b/cypress/integration/free/maintainer.spec.ts @@ -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(); diff --git a/cypress/integration/free/observer.spec.ts b/cypress/integration/free/observer.spec.ts index 2cb5817917..107679173a 100644 --- a/cypress/integration/free/observer.spec.ts +++ b/cypress/integration/free/observer.spec.ts @@ -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(() => { diff --git a/cypress/integration/premium/admin.spec.ts b/cypress/integration/premium/admin.spec.ts index 7e8c58b2a0..6f554a3322 100644 --- a/cypress/integration/premium/admin.spec.ts +++ b/cypress/integration/premium/admin.spec.ts @@ -36,20 +36,8 @@ 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… diff --git a/cypress/integration/premium/maintainer.spec.ts b/cypress/integration/premium/maintainer.spec.ts index 005e3ac9f4..38db40e71f 100644 --- a/cypress/integration/premium/maintainer.spec.ts +++ b/cypress/integration/premium/maintainer.spec.ts @@ -29,10 +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(); - // TODO: Check Team Apples is in Select a team dropdown + cy.contains("button", /generate installer/i).click(); cy.contains("button", /done/i).click(); // Host details page: Can see team UI diff --git a/cypress/integration/premium/observer.spec.ts b/cypress/integration/premium/observer.spec.ts index 5f5f052cfe..c96969a56e 100644 --- a/cypress/integration/premium/observer.spec.ts +++ b/cypress/integration/premium/observer.spec.ts @@ -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 diff --git a/cypress/integration/premium/team_maintainer_observer.spec.ts b/cypress/integration/premium/team_maintainer_observer.spec.ts index dde92d6097..dc91c12d23 100644 --- a/cypress/integration/premium/team_maintainer_observer.spec.ts +++ b/cypress/integration/premium/team_maintainer_observer.spec.ts @@ -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,17 +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… diff --git a/frontend/components/App/App.tsx b/frontend/components/App/App.tsx index 28abfb2158..964a19020b 100644 --- a/frontend/components/App/App.tsx +++ b/frontend/components/App/App.tsx @@ -13,11 +13,18 @@ import { IConfig } from "interfaces/config"; import TableProvider from "context/table"; import QueryProvider from "context/query"; import { AppContext } from "context/app"; +import { IEnrollSecret } from "interfaces/enroll_secret"; interface IAppProps { children: JSX.Element; } +interface ISecretResponse { + spec: { + secrets: IEnrollSecret[]; + }; +} + interface IRootState { auth: { user: IUser; @@ -31,10 +38,12 @@ const App = ({ children }: IAppProps) => { const { setCurrentUser, setConfig, + setEnrollSecret, currentUser, isGlobalObserver, isOnlyObserver, isAnyTeamMaintainer, + enrollSecret, } = useContext(AppContext); useDeepEffect(() => { @@ -62,7 +71,11 @@ const App = ({ children }: IAppProps) => { !isAnyTeamMaintainer; if (canGetEnrollSecret) { - dispatch(getEnrollSecret()).catch(() => false); + dispatch(getEnrollSecret()) + .then((response: ISecretResponse) => { + setEnrollSecret(response.spec.secrets); + }) + .catch(() => false); } }, [currentUser, isGlobalObserver, isOnlyObserver]); diff --git a/frontend/context/app.tsx b/frontend/context/app.tsx index 53ad83fb62..6d572a75d8 100644 --- a/frontend/context/app.tsx +++ b/frontend/context/app.tsx @@ -4,6 +4,7 @@ import { IUser } from "interfaces/user"; import { IConfig } from "interfaces/config"; import { ITeam } from "interfaces/team"; import permissions from "utilities/permissions"; +import { IEnrollSecret } from "interfaces/enroll_secret"; type Props = { children: ReactNode; @@ -13,6 +14,7 @@ type InitialStateType = { config: IConfig | null; currentUser: IUser | null; currentTeam: ITeam | undefined; + enrollSecret: IEnrollSecret[] | null; isFreeTier: boolean | undefined; isPremiumTier: boolean | undefined; isGlobalAdmin: boolean | undefined; @@ -25,12 +27,14 @@ type InitialStateType = { setCurrentUser: (user: IUser) => void; setCurrentTeam: (team: ITeam | undefined) => void; setConfig: (config: IConfig) => void; + setEnrollSecret: (enrollSecret: IEnrollSecret[]) => void; }; const initialState = { config: null, currentUser: null, currentTeam: undefined, + enrollSecret: null, isFreeTier: undefined, isPremiumTier: undefined, isGlobalAdmin: undefined, @@ -43,12 +47,14 @@ const initialState = { setCurrentUser: () => null, setCurrentTeam: () => null, setConfig: () => null, + setEnrollSecret: () => null, }; const actions = { SET_CURRENT_USER: "SET_CURRENT_USER", SET_CURRENT_TEAM: "SET_CURRENT_TEAM", SET_CONFIG: "SET_CONFIG", + SET_ENROLL_SECRET: "SET_ENROLL_SECRET", }; // helper function - this is run every @@ -95,6 +101,11 @@ const reducer = (state: any, action: any) => { config: action.config, ...setPermissions(state.currentUser, action.config), }; + case actions.SET_ENROLL_SECRET: + return { + ...state, + enrollSecret: action.enrollSecret, + }; default: return state; } @@ -109,6 +120,7 @@ const AppProvider = ({ children }: Props) => { config: state.config, currentUser: state.currentUser, currentTeam: state.currentTeam, + enrollSecret: state.enrollSecret, isFreeTier: state.isFreeTier, isPremiumTier: state.isPremiumTier, isGlobalAdmin: state.isGlobalAdmin, @@ -127,6 +139,9 @@ const AppProvider = ({ children }: Props) => { setConfig: (config: IConfig) => { dispatch({ type: actions.SET_CONFIG, config }); }, + setEnrollSecret: (enrollSecret: IEnrollSecret[]) => { + dispatch({ type: actions.SET_ENROLL_SECRET, enrollSecret }); + }, }; return {children}; diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index ea2760e24d..0f1fd97d82 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -63,13 +63,13 @@ import { getValidatedTeamId, } from "./helpers"; import EnrollSecretModal from "./components/EnrollSecretModal"; // @ts-ignore -import AddHostModal from "./components/AddHostModal"; import NoHosts from "./components/NoHosts"; import EmptyHosts from "./components/EmptyHosts"; import PoliciesFilter from "./components/PoliciesFilter"; // @ts-ignore import EditColumnsModal from "./components/EditColumnsModal/EditColumnsModal"; import TransferHostModal from "./components/TransferHostModal"; import DeleteHostModal from "./components/DeleteHostModal"; +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"; @@ -124,6 +124,7 @@ const ManageHostsPage = ({ isPremiumTier, currentTeam, setCurrentTeam, + enrollSecret: globalSecret, } = useContext(AppContext); const { selectedOsqueryTable, setSelectedOsqueryTable } = useContext( QueryContext @@ -150,7 +151,6 @@ const ManageHostsPage = ({ // ========= states const [selectedLabel, setSelectedLabel] = useState(); const [statusLabels, setStatusLabels] = useState(); - const [showAddHostModal, setShowAddHostModal] = useState(false); const [showEnrollSecretModal, setShowEnrollSecretModal] = useState( false ); @@ -166,6 +166,10 @@ const ManageHostsPage = ({ const [showDeleteHostModal, setShowDeleteHostModal] = useState( false ); + const [ + showGenerateInstallerModal, + setShowGenerateInstallerModal, + ] = useState(false); const [hiddenColumns, setHiddenColumns] = useState( storedHiddenColumns || defaultHiddenColumns ); @@ -206,6 +210,11 @@ const ManageHostsPage = ({ isGlobalAdmin || isGlobalMaintainer || isTeamMaintainer; const canAddNewLabels = isGlobalAdmin || isGlobalMaintainer; + const generateInstallerTeam = currentTeam || { + name: "No team", + secrets: globalSecret, + }; + const { isLoading: isLabelsLoading, data: labels, @@ -268,6 +277,10 @@ const ManageHostsPage = ({ setShowDeleteHostModal(!showDeleteHostModal); }; + const toggleGenerateInstallerModal = () => { + setShowGenerateInstallerModal(!showGenerateInstallerModal); + }; + const toggleAllMatchingHosts = (shouldSelect: boolean) => { if (typeof shouldSelect !== "undefined") { setIsAllMatchingHostsSelected(shouldSelect); @@ -930,27 +943,6 @@ const ManageHostsPage = ({ ); }; - const renderAddHostModal = () => { - if (!canAddNewHosts || !showAddHostModal) { - return null; - } - - return ( - setShowAddHostModal(false)} - className={`${baseClass}__invite-modal`} - > - setShowAddHostModal(false)} - config={config} - currentUser={currentUser} - /> - - ); - }; - const renderDeleteLabelModal = () => { if (!showDeleteLabelModal) { return false; @@ -1007,6 +999,19 @@ const ManageHostsPage = ({ ); }; + const renderGenerateInstallerModal = () => { + if (!showGenerateInstallerModal) { + return null; + } + + return ( + + ); + }; + const renderHeaderLabelBlock = ({ description, display_text: displayText, @@ -1139,7 +1144,7 @@ const ManageHostsPage = ({ ); }; - const renderTable = () => { + const renderTable = (selectedTeam: number) => { if ( !config || !currentUser || @@ -1155,8 +1160,15 @@ const ManageHostsPage = ({ } // Hosts have not been set up for this instance yet. - if (getStatusSelected() === ALL_HOSTS_LABEL && selectedLabel.count === 0) { - return ; + if ( + (getStatusSelected() === ALL_HOSTS_LABEL && selectedLabel.count === 0) || + (getStatusSelected() === ALL_HOSTS_LABEL && + filteredHostCount === 0 && + searchQuery === "") + ) { + return ( + + ); } const secondarySelectActions: IActionButtonProps[] = [ @@ -1212,6 +1224,8 @@ const ManageHostsPage = ({ ); }; + const selectedTeam = currentTeam?.id || 0; + return (
{renderForm()} @@ -1226,30 +1240,38 @@ const ManageHostsPage = ({ className={`${baseClass}__enroll-hosts button`} variant="inverse" > - Show enroll secret - - )} - {canAddNewHosts && ( - )} + {canAddNewHosts && + !( + getStatusSelected() === ALL_HOSTS_LABEL && + selectedLabel?.count === 0 + ) && + !( + getStatusSelected() === ALL_HOSTS_LABEL && + filteredHostCount === 0 + ) && ( + + )}
{renderLabelOrPolicyBlock()} - {config && (!isPremiumTier || teams) && renderTable()} + {config && (!isPremiumTier || teams) && renderTable(selectedTeam)} )} {!isLabelsLoading && renderSidePanel()} {renderEnrollSecretModal()} - {renderAddHostModal()} {renderEditColumnsModal()} {renderDeleteLabelModal()} {renderTransferHostModal()} {renderDeleteHostModal()} + {renderGenerateInstallerModal()} ); }; diff --git a/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/AddHostModal.jsx b/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/AddHostModal.jsx deleted file mode 100644 index 87ae9e2dca..0000000000 --- a/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/AddHostModal.jsx +++ /dev/null @@ -1,378 +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/config/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 ( - - - {copyMessage && {`${copyMessage} `}} - - - - ); - }; - - 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 ( -
-
- -
    -
  1. -

    - 1Enroll - secret -

    -

    - Osquery uses an enroll secret to authenticate with the Fleet - server. -

    -
    - {isPremiumTier && currentUserTeams ? ( - - ) : null} - {isPremiumTier && selectedTeam && ( - - )} - {!isPremiumTier && ( - - )} -
    -
  2. -
  3. -

    - 2Server - certificate -

    -

    - Provide the TLS certificate used by the Fleet server to enable - secure connections from osquery: -

    -

    - {fetchCertificateError ? ( - - {fetchCertificateError} - - ) : ( - - Download - download icon - - )} -

    -
  4. -
  5. -

    - 3Flagfile -

    -

    - If using the enroll secret and server certificate downloaded - above, use the generated flagfile. In some configurations, - modifications may need to be made: -

    -

    - - Download - download icon - -

    -
  6. -
  7. -

    - 4Run - osquery -

    -

    - Run osquery from the directory containing the above files (may - require sudo or Run as Administrator privileges): -

    -
    - -
    -
  8. -
-
- -
- -
-
- ); - } -} - -export default AddHostModal; diff --git a/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/_styles.scss deleted file mode 100644 index 8b9312ecf7..0000000000 --- a/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/_styles.scss +++ /dev/null @@ -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; - } - } -} diff --git a/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/index.js b/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/index.js deleted file mode 100644 index 999c5a522d..0000000000 --- a/frontend/pages/hosts/ManageHostsPage/components/AddHostModal/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./AddHostModal"; diff --git a/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/GenerateInstallerModal.tsx b/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/GenerateInstallerModal.tsx new file mode 100644 index 0000000000..86bad3a6a4 --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/GenerateInstallerModal.tsx @@ -0,0 +1,27 @@ +import React, { useState } from "react"; + +import Button from "components/buttons/Button"; +import Modal from "components/modals/Modal"; +import { ITeam } from "interfaces/team"; +import { IEnrollSecret } from "interfaces/enroll_secret"; +import PlatformWrapper from "./PlatformWrapper/PlatformWrapper"; + +const baseClass = "generate-installer-modal"; + +interface IGenerateInstallerModal { + onCancel: () => void; + selectedTeam: ITeam | { name: string; secrets: IEnrollSecret[] | null }; +} + +const GenerateInstallerModal = ({ + onCancel, + selectedTeam, +}: IGenerateInstallerModal): JSX.Element => { + return ( + + + + ); +}; + +export default GenerateInstallerModal; diff --git a/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/PlatformWrapper/PlatformWrapper.tsx b/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/PlatformWrapper/PlatformWrapper.tsx new file mode 100644 index 0000000000..c708124697 --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/PlatformWrapper/PlatformWrapper.tsx @@ -0,0 +1,217 @@ +import React, { useState } from "react"; +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; + +// @ts-ignore +import Fleet from "fleet"; +import Button from "components/buttons/Button"; +// @ts-ignore +import InputField from "components/forms/fields/InputField"; +// @ts-ignore +import { stringToClipboard } from "utilities/copy_text"; +import FileSaver from "file-saver"; +import { ITeam } from "interfaces/team"; +import { IEnrollSecret } from "interfaces/enroll_secret"; +import CopyIcon from "../../../../../../../assets/images/icon-copy-clipboard-fleet-blue-20x20@2x.png"; +import DownloadIcon from "../../../../../../../assets/images/icon-download-12x12@2x.png"; + +interface IPlatformSubNav { + name: string; + type: string; +} + +const platformSubNav: IPlatformSubNav[] = [ + { + name: "macOS", + type: "pkg", + }, + { + name: "Windows", + type: "msi", + }, + { + name: "Linux (RPM)", + type: "rpm", + }, + { + name: "Linux (DEB)", + type: "deb", + }, +]; + +interface IPlatformWrapperProp { + selectedTeam: ITeam | { name: string; secrets: IEnrollSecret[] | null }; + onCancel: () => void; +} + +const baseClass = "platform-wrapper"; + +const PlatformWrapper = ({ + selectedTeam, + onCancel, +}: IPlatformWrapperProp): JSX.Element => { + const [copyMessage, setCopyMessage] = useState(""); + const [certificate, setCertificate] = useState(undefined); + const [fetchCertificateError, setFetchCertificateError] = useState< + string | undefined + >(undefined); + + Fleet.config + .loadCertificate() + .then((loadedCertificate: any) => { + setCertificate(loadedCertificate); + }) + .catch(() => { + setFetchCertificateError( + "Failed to load certificate. Is Fleet app URL configured properly?" + ); + }); + + const onDownloadCertificate = (evt: React.MouseEvent) => { + evt.preventDefault(); + + if (certificate) { + const filename = "fleet.pem"; + const file = new global.window.File([certificate], filename, { + type: "application/x-pem-file", + }); + + FileSaver.saveAs(file); + } + return false; + }; + + const renderInstallerString = (platform: string) => { + let enrollSecret; + if (selectedTeam.secrets) { + enrollSecret = selectedTeam.secrets[0].secret; + } + + let installerString = `fleetctl package --type=${platform} --fleet-url=https://localhost:8412 --enroll-secret=${enrollSecret}`; + if (platform === "rpm" || platform === "deb") { + installerString += + " --fleet-certificate=/home/username/Downloads/fleet.pem"; + } + return installerString; + }; + + const renderLabel = (installerString: string) => { + const onCopyInstaller = (evt: React.MouseEvent) => { + evt.preventDefault(); + + stringToClipboard(installerString) + .then(() => setCopyMessage("Copied!")) + .catch(() => setCopyMessage("Copy failed")); + + // Clear message after 1 second + setTimeout(() => setCopyMessage(""), 1000); + + return false; + }; + + return ( + <> + + With the{" "} + + Fleet command-line tool + {" "} + installed: + {" "} + + + {copyMessage && {`${copyMessage} `}} + + + + + ); + }; + + const renderTab = (platform: string) => { + return ( + <> + {(platform === "rpm" || platform === "deb") && ( + <> + + Download your Fleet certificate: + +

+ {fetchCertificateError ? ( + + {fetchCertificateError} + + ) : ( + + Download + download + + )} +

+ + )} + + + Generates an installer that your devices will use to connect to Fleet. + + + ); + }; + + return ( +
+
+ + + {platformSubNav.map((navItem) => { + // Bolding text when the tab is active causes a layout shift + // so we add a hidden pseudo element with the same text string + return ( + + {navItem.name} + + ); + })} + + {platformSubNav.map((navItem) => { + // Bolding text when the tab is active causes a layout shift + // so we add a hidden pseudo element with the same text string + return ( + + {renderTab(navItem.type)} + + ); + })} + +
+
+ +
+
+ ); +}; + +export default PlatformWrapper; diff --git a/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/PlatformWrapper/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/PlatformWrapper/_styles.scss new file mode 100644 index 0000000000..6ea8e0bfeb --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/PlatformWrapper/_styles.scss @@ -0,0 +1,153 @@ +@import "../../../../../../../node_modules/react-tabs/style/react-tabs.scss"; + +.platform-wrapper { + padding: 0; // different to pad sticky subnav properly + + h1 { + margin-bottom: $pad-small; + } + + a { + font-size: $x-small; + color: $core-vibrant-blue; + font-weight: $bold; + text-decoration: none; + + img { + transform: scale(0.5); + vertical-align: bottom; + } + } + + p { + margin-top: 0; + } + + .body-wrap { + padding: 0; + } + + .react-tabs { + .react-tabs__tab-list { + border-bottom: 1px solid $ui-gray; + } + + &__tab { + font-size: $x-small; + border: none; + padding: 0 0 $pad-medium 0; + margin-left: $pad-xxlarge; + display: inline-flex; + flex-direction: column; + align-items: center; + + &:focus { + box-shadow: none; + outline: 0; + } + + // Bolding text when the tab is active causes a layout shift + // so we add a hidden pseudo element with the same text string + &:before { + content: attr(data-text); + height: 0; + visibility: hidden; + overflow: hidden; + user-select: none; + pointer-events: none; + font-weight: $bold; + } + + &:after { + background-color: transparent; + } + + &:first-child { + margin-left: 0; + } + } + + &__tab--selected, + &__tab--selected:focus { + font-weight: $bold; + border-bottom: 1px solid $core-vibrant-blue; + } + } + + &__info { + margin-top: $pad-small; + } + + &__cta { + font-weight: $bold; + } + + .form-field { + margin-bottom: $pad-small; + + &__label { + position: relative; + font-size: $x-small; + font-weight: $bold; + margin-top: $pad-medium; + } + } + + .input-field { + &--disabled { + letter-spacing: 0; + } + + &__textarea { + font-family: "SourceCodePro", $monospace; + font-weight: 600; + font-size: 13px; + color: $core-fleet-blue; + min-height: 70px; + line-height: 20px; + margin-bottom: 0; + padding: $pad-medium $pad-large $pad-medium $pad-medium; + } + } + + &__installer-input-deb, + &__installer-input-rpm { + .input-field__textarea { + min-height: 95px; + } + } + + &__installer-copy-icon { + color: $core-vibrant-blue; + margin-left: $pad-small; + margin-right: $pad-medium; + } + + .buttons { + display: flex; + align-items: center; + position: absolute; + right: 0; + top: 40px; + + span { + font-weight: $regular; + } + + a { + display: flex; + align-items: center; + } + + img { + width: 16px; + height: 16px; + } + } + + &__button-wrap { + display: flex; + justify-content: flex-end; + margin: $pad-xxlarge 0 0; + } +} diff --git a/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/_styles.scss new file mode 100644 index 0000000000..3f597f1c64 --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/_styles.scss @@ -0,0 +1,11 @@ +.generate-installer-modal { + &__btn-wrap { + display: flex; + flex-direction: row-reverse; + margin-top: $pad-xxlarge; + } + + &__btn { + margin-left: 12px; + } +} diff --git a/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/index.ts b/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/index.ts new file mode 100644 index 0000000000..39535acccc --- /dev/null +++ b/frontend/pages/hosts/ManageHostsPage/components/GenerateInstallerModal/index.ts @@ -0,0 +1 @@ +export { default } from "./GenerateInstallerModal"; diff --git a/frontend/pages/hosts/ManageHostsPage/components/NoHosts/NoHosts.tsx b/frontend/pages/hosts/ManageHostsPage/components/NoHosts/NoHosts.tsx index d002b09a84..1a125d3b92 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/NoHosts/NoHosts.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/NoHosts/NoHosts.tsx @@ -2,25 +2,35 @@ * Component when there is no hosts set up in fleet */ import React from "react"; +import Button from "components/buttons/Button"; +import { ITeam } from "interfaces/team"; import RoboDogImage from "../../../../../../assets/images/robo-dog-176x144@2x.png"; +interface INoHostsProps { + toggleGenerateInstallerModal: () => void; +} + const baseClass = "no-hosts"; -const NoHosts = (): JSX.Element => { +const NoHosts = ({ + toggleGenerateInstallerModal, +}: INoHostsProps): JSX.Element => { return (
No Hosts
-

It's kinda empty in here...

-

Get started adding hosts to Fleet.

-

Add your laptops and servers to securely monitor them.

-
-

Still having trouble?

- - File a GitHub issue - +

Add your devices to Fleet

+

Generate an installer to add your own devices.

+
+
diff --git a/frontend/pages/hosts/ManageHostsPage/components/NoHosts/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/NoHosts/_styles.scss index 70a8bf716b..23ab91fe5a 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/NoHosts/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/components/NoHosts/_styles.scss @@ -86,4 +86,8 @@ text-decoration: none; } } + + &__no-hosts-button { + margin-top: $pad-large; + } }