Manage Host Page: No host renders generate installer modal (#2493)
* Update Context API to contain enrollSecret * Update e2e tests with generate installer modal * Edge case with empty Team showing Generate Installer CTA
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 502 B |
@@ -0,0 +1 @@
|
||||
* New CTA for new users to generate installer to add hosts onto Fleet
|
||||
@@ -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");
|
||||
// });
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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…
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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…
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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 <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
|
||||
@@ -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<ILabel>();
|
||||
const [statusLabels, setStatusLabels] = useState<IStatusLabels>();
|
||||
const [showAddHostModal, setShowAddHostModal] = useState<boolean>(false);
|
||||
const [showEnrollSecretModal, setShowEnrollSecretModal] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
@@ -166,6 +166,10 @@ const ManageHostsPage = ({
|
||||
const [showDeleteHostModal, setShowDeleteHostModal] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [
|
||||
showGenerateInstallerModal,
|
||||
setShowGenerateInstallerModal,
|
||||
] = useState<boolean>(false);
|
||||
const [hiddenColumns, setHiddenColumns] = useState<string[]>(
|
||||
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 (
|
||||
<Modal
|
||||
title="New host"
|
||||
onExit={() => setShowAddHostModal(false)}
|
||||
className={`${baseClass}__invite-modal`}
|
||||
>
|
||||
<AddHostModal
|
||||
teams={teams}
|
||||
onReturnToApp={() => setShowAddHostModal(false)}
|
||||
config={config}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDeleteLabelModal = () => {
|
||||
if (!showDeleteLabelModal) {
|
||||
return false;
|
||||
@@ -1007,6 +999,19 @@ const ManageHostsPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const renderGenerateInstallerModal = () => {
|
||||
if (!showGenerateInstallerModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<GenerateInstallerModal
|
||||
onCancel={toggleGenerateInstallerModal}
|
||||
selectedTeam={generateInstallerTeam}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 <NoHosts />;
|
||||
if (
|
||||
(getStatusSelected() === ALL_HOSTS_LABEL && selectedLabel.count === 0) ||
|
||||
(getStatusSelected() === ALL_HOSTS_LABEL &&
|
||||
filteredHostCount === 0 &&
|
||||
searchQuery === "")
|
||||
) {
|
||||
return (
|
||||
<NoHosts toggleGenerateInstallerModal={toggleGenerateInstallerModal} />
|
||||
);
|
||||
}
|
||||
|
||||
const secondarySelectActions: IActionButtonProps[] = [
|
||||
@@ -1212,6 +1224,8 @@ const ManageHostsPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const selectedTeam = currentTeam?.id || 0;
|
||||
|
||||
return (
|
||||
<div className="has-sidebar">
|
||||
{renderForm()}
|
||||
@@ -1226,30 +1240,38 @@ const ManageHostsPage = ({
|
||||
className={`${baseClass}__enroll-hosts button`}
|
||||
variant="inverse"
|
||||
>
|
||||
<span>Show enroll secret</span>
|
||||
</Button>
|
||||
)}
|
||||
{canAddNewHosts && (
|
||||
<Button
|
||||
onClick={() => setShowAddHostModal(true)}
|
||||
className={`${baseClass}__add-hosts button button--brand`}
|
||||
>
|
||||
<span>Add new host</span>
|
||||
<span>Manage enroll secret</span>
|
||||
</Button>
|
||||
)}
|
||||
{canAddNewHosts &&
|
||||
!(
|
||||
getStatusSelected() === ALL_HOSTS_LABEL &&
|
||||
selectedLabel?.count === 0
|
||||
) &&
|
||||
!(
|
||||
getStatusSelected() === ALL_HOSTS_LABEL &&
|
||||
filteredHostCount === 0
|
||||
) && (
|
||||
<Button
|
||||
onClick={toggleGenerateInstallerModal}
|
||||
className={`${baseClass}__add-hosts button button--brand`}
|
||||
>
|
||||
<span>Generate installer</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{renderLabelOrPolicyBlock()}
|
||||
{config && (!isPremiumTier || teams) && renderTable()}
|
||||
{config && (!isPremiumTier || teams) && renderTable(selectedTeam)}
|
||||
</div>
|
||||
)}
|
||||
{!isLabelsLoading && renderSidePanel()}
|
||||
{renderEnrollSecretModal()}
|
||||
{renderAddHostModal()}
|
||||
{renderEditColumnsModal()}
|
||||
{renderDeleteLabelModal()}
|
||||
{renderTransferHostModal()}
|
||||
{renderDeleteHostModal()}
|
||||
{renderGenerateInstallerModal()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<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";
|
||||
+27
@@ -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 (
|
||||
<Modal onExit={onCancel} title={"Generate installer"} className={baseClass}>
|
||||
<PlatformWrapper onCancel={onCancel} selectedTeam={selectedTeam} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateInstallerModal;
|
||||
+217
@@ -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<string>("");
|
||||
const [certificate, setCertificate] = useState<string | undefined>(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 (
|
||||
<>
|
||||
<span className={`${baseClass}__cta`}>
|
||||
With the{" "}
|
||||
<a
|
||||
href="https://fleetdm.com/get-started"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`${baseClass}__command-line-tool`}
|
||||
>
|
||||
Fleet command-line tool
|
||||
</a>{" "}
|
||||
installed:
|
||||
</span>{" "}
|
||||
<span className={`${baseClass}__name`}>
|
||||
<span className="buttons">
|
||||
{copyMessage && <span>{`${copyMessage} `}</span>}
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className={`${baseClass}__installer-copy-icon`}
|
||||
onClick={onCopyInstaller}
|
||||
>
|
||||
<img src={CopyIcon} alt="copy" />
|
||||
</Button>
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTab = (platform: string) => {
|
||||
return (
|
||||
<>
|
||||
{(platform === "rpm" || platform === "deb") && (
|
||||
<>
|
||||
<span className={`${baseClass}__cta`}>
|
||||
Download your Fleet certificate:
|
||||
</span>
|
||||
<p>
|
||||
{fetchCertificateError ? (
|
||||
<span className={`${baseClass}__error`}>
|
||||
{fetchCertificateError}
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
href="#downloadCertificate"
|
||||
className={`${baseClass}__fleet-certificate-download`}
|
||||
onClick={onDownloadCertificate}
|
||||
>
|
||||
Download
|
||||
<img src={DownloadIcon} alt="download" />
|
||||
</a>
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<InputField
|
||||
disabled
|
||||
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__installer-input-${platform}`}
|
||||
name="installer"
|
||||
label={renderLabel(renderInstallerString(platform))}
|
||||
type={"textarea"}
|
||||
value={renderInstallerString(platform)}
|
||||
/>
|
||||
<span>
|
||||
Generates an installer that your devices will use to connect to Fleet.
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__nav-header`}>
|
||||
<Tabs>
|
||||
<TabList>
|
||||
{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 (
|
||||
<Tab key={navItem.name} data-text={navItem.name}>
|
||||
{navItem.name}
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</TabList>
|
||||
{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 (
|
||||
<TabPanel className={`${baseClass}__info`} key={navItem.type}>
|
||||
{renderTab(navItem.type)}
|
||||
</TabPanel>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className={`${baseClass}__button-wrap`}>
|
||||
<Button onClick={onCancel} className="button button--brand">
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlatformWrapper;
|
||||
+153
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.generate-installer-modal {
|
||||
&__btn-wrap {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
margin-top: $pad-xxlarge;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./GenerateInstallerModal";
|
||||
@@ -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 (
|
||||
<div className={`${baseClass}`}>
|
||||
<div className={`${baseClass}__inner`}>
|
||||
<img src={RoboDogImage} alt="No Hosts" />
|
||||
<div>
|
||||
<h1>It's kinda empty in here...</h1>
|
||||
<h2>Get started adding hosts to Fleet.</h2>
|
||||
<p>Add your laptops and servers to securely monitor them.</p>
|
||||
<div className={`${baseClass}__no-hosts-contact`}>
|
||||
<p>Still having trouble?</p>
|
||||
<a href="https://github.com/fleetdm/fleet/issues">
|
||||
File a GitHub issue
|
||||
</a>
|
||||
<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={toggleGenerateInstallerModal}
|
||||
type="button"
|
||||
className="button button--brand"
|
||||
>
|
||||
Generate installer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,4 +86,8 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__no-hosts-button {
|
||||
margin-top: $pad-large;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user