mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Sandbox UI: Homepage, CTA to add personal device (#6775)
This commit is contained in:
parent
8d3de2a70a
commit
08485b3a5c
5 changed files with 146 additions and 61 deletions
1
changes/issue-5911-sandbox-homepage
Normal file
1
changes/issue-5911-sandbox-homepage
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Dashboard updates for app, Fleet Sandbox, and Fleet Preview
|
||||
|
|
@ -3,10 +3,15 @@ import { useQuery } from "react-query";
|
|||
import { AppContext } from "context/app";
|
||||
import { find } from "lodash";
|
||||
|
||||
import {
|
||||
IEnrollSecret,
|
||||
IEnrollSecretsResponse,
|
||||
} from "interfaces/enroll_secret";
|
||||
import { IHostSummary, IHostSummaryPlatforms } from "interfaces/host_summary";
|
||||
import { ILabelSummary } from "interfaces/label";
|
||||
import { IOsqueryPlatform } from "interfaces/platform";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import enrollSecretsAPI from "services/entities/enroll_secret";
|
||||
import hostSummaryAPI from "services/entities/host_summary";
|
||||
import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams";
|
||||
import sortUtils from "utilities/sort";
|
||||
|
|
@ -26,6 +31,7 @@ import WelcomeHost from "./cards/WelcomeHost";
|
|||
import MDM from "./cards/MDM";
|
||||
import Munki from "./cards/Munki";
|
||||
import OperatingSystems from "./cards/OperatingSystems";
|
||||
import AddHostsModal from "../../components/AddHostsModal";
|
||||
import ExternalURLIcon from "../../../assets/images/icon-external-url-12x12@2x.png";
|
||||
|
||||
const baseClass = "homepage";
|
||||
|
|
@ -34,9 +40,13 @@ const Homepage = (): JSX.Element => {
|
|||
const {
|
||||
config,
|
||||
currentTeam,
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isTeamAdmin,
|
||||
isTeamMaintainer,
|
||||
isPremiumTier,
|
||||
isFreeTier,
|
||||
isPreviewMode,
|
||||
isSandboxMode,
|
||||
isOnGlobalTeam,
|
||||
setCurrentTeam,
|
||||
} = useContext(AppContext);
|
||||
|
|
@ -54,25 +64,30 @@ const Homepage = (): JSX.Element => {
|
|||
const [showSoftwareUI, setShowSoftwareUI] = useState<boolean>(false);
|
||||
const [showMunkiUI, setShowMunkiUI] = useState<boolean>(false);
|
||||
const [showMDMUI, setShowMDMUI] = useState<boolean>(false);
|
||||
const [showAddHostsModal, setShowAddHostsModal] = useState<boolean>(false);
|
||||
const [showOperatingSystemsUI, setShowOperatingSystemsUI] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [showHostsUI, setShowHostsUI] = useState<boolean>(false); // Hides UI on first load only
|
||||
|
||||
const { data: teams } = useQuery<ILoadTeamsResponse, Error, ITeam[]>(
|
||||
["teams"],
|
||||
() => teamsAPI.loadAll(),
|
||||
{
|
||||
enabled: !!isPremiumTier,
|
||||
select: (data: ILoadTeamsResponse) =>
|
||||
data.teams.sort((a, b) => sortUtils.caseInsensitiveAsc(a.name, b.name)),
|
||||
onSuccess: (responseTeams) => {
|
||||
if (!currentTeam && !isOnGlobalTeam && responseTeams.length) {
|
||||
setCurrentTeam(responseTeams[0]);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
const canEnrollHosts =
|
||||
isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer;
|
||||
const canEnrollGlobalHosts = isGlobalAdmin || isGlobalMaintainer;
|
||||
|
||||
const { data: teams, isLoading: isLoadingTeams } = useQuery<
|
||||
ILoadTeamsResponse,
|
||||
Error,
|
||||
ITeam[]
|
||||
>(["teams"], () => teamsAPI.loadAll(), {
|
||||
enabled: !!isPremiumTier,
|
||||
select: (data: ILoadTeamsResponse) =>
|
||||
data.teams.sort((a, b) => sortUtils.caseInsensitiveAsc(a.name, b.name)),
|
||||
onSuccess: (responseTeams) => {
|
||||
if (!currentTeam && !isOnGlobalTeam && responseTeams.length) {
|
||||
setCurrentTeam(responseTeams[0]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: hostSummaryData,
|
||||
|
|
@ -108,11 +123,28 @@ const Homepage = (): JSX.Element => {
|
|||
}
|
||||
);
|
||||
|
||||
const {
|
||||
isLoading: isGlobalSecretsLoading,
|
||||
data: globalSecrets,
|
||||
refetch: refetchGlobalSecrets,
|
||||
} = useQuery<IEnrollSecretsResponse, Error, IEnrollSecret[]>(
|
||||
["global secrets"],
|
||||
() => enrollSecretsAPI.getGlobalEnrollSecrets(),
|
||||
{
|
||||
enabled: !!canEnrollGlobalHosts,
|
||||
select: (data: IEnrollSecretsResponse) => data.secrets,
|
||||
}
|
||||
);
|
||||
|
||||
const handleTeamSelect = (teamId: number) => {
|
||||
const selectedTeam = find(teams, ["id", teamId]);
|
||||
setCurrentTeam(selectedTeam);
|
||||
};
|
||||
|
||||
const toggleAddHostsModal = () => {
|
||||
setShowAddHostsModal(!showAddHostsModal);
|
||||
};
|
||||
|
||||
const HostsSummaryCard = useInfoCard({
|
||||
title: "Hosts",
|
||||
action: {
|
||||
|
|
@ -156,11 +188,20 @@ const Homepage = (): JSX.Element => {
|
|||
|
||||
const WelcomeHostCard = useInfoCard({
|
||||
title: "Welcome to Fleet",
|
||||
children: <WelcomeHost />,
|
||||
showTitle: true,
|
||||
children: (
|
||||
<WelcomeHost
|
||||
totalsHostsCount={
|
||||
(hostSummaryData && hostSummaryData.totals_hosts_count) || 0
|
||||
}
|
||||
toggleAddHostsModal={toggleAddHostsModal}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
const LearnFleetCard = useInfoCard({
|
||||
title: "Learn how to use Fleet",
|
||||
showTitle: true,
|
||||
children: <LearnFleet />,
|
||||
});
|
||||
|
||||
|
|
@ -252,16 +293,14 @@ const Homepage = (): JSX.Element => {
|
|||
|
||||
const allLayout = () => (
|
||||
<div className={`${baseClass}__section`}>
|
||||
{isPreviewMode && (
|
||||
{hostSummaryData && hostSummaryData?.totals_hosts_count < 2 && (
|
||||
<>
|
||||
{WelcomeHostCard}
|
||||
{LearnFleetCard}
|
||||
</>
|
||||
)}
|
||||
{SoftwareCard}
|
||||
{!isPreviewMode && !currentTeam && isOnGlobalTeam && (
|
||||
<>{ActivityFeedCard}</>
|
||||
)}
|
||||
{!currentTeam && isOnGlobalTeam && <>{ActivityFeedCard}</>}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -289,6 +328,19 @@ const Homepage = (): JSX.Element => {
|
|||
}
|
||||
};
|
||||
|
||||
const renderAddHostsModal = () => {
|
||||
const enrollSecret = globalSecrets?.[0].secret;
|
||||
return (
|
||||
<AddHostsModal
|
||||
currentTeam={currentTeam}
|
||||
enrollSecret={enrollSecret}
|
||||
isLoading={isLoadingTeams || isGlobalSecretsLoading}
|
||||
isSandboxMode={!!isSandboxMode}
|
||||
onCancel={toggleAddHostsModal}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__wrapper body-wrap`}>
|
||||
|
|
@ -336,6 +388,7 @@ const Homepage = (): JSX.Element => {
|
|||
</>
|
||||
</div>
|
||||
{renderCards()}
|
||||
{showAddHostsModal && renderAddHostsModal()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -145,11 +145,10 @@ const ActivityFeed = ({
|
|||
return (
|
||||
<div className={`${baseClass}__no-activities`}>
|
||||
<p>
|
||||
<b>This is the start of your Fleet activities.</b>
|
||||
<b>Fleet has not recorded any activity.</b>
|
||||
</p>
|
||||
<p>
|
||||
Did you recently edit your queries, update your packs, or run a live
|
||||
query? Try again in a few seconds as the system catches up.
|
||||
Try editing a query, updating your policies, or running a live query.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,12 +24,20 @@ interface IHostResponse {
|
|||
host: IHost;
|
||||
}
|
||||
|
||||
interface IWelcomeHostCardProps {
|
||||
totalsHostsCount: number;
|
||||
toggleAddHostsModal: (showAddHostsModal: boolean) => void;
|
||||
}
|
||||
|
||||
const baseClass = "welcome-host";
|
||||
const HOST_ID = 1;
|
||||
const policyPass = "pass";
|
||||
const policyFail = "fail";
|
||||
|
||||
const WelcomeHost = (): JSX.Element => {
|
||||
const WelcomeHost = ({
|
||||
totalsHostsCount,
|
||||
toggleAddHostsModal,
|
||||
}: IWelcomeHostCardProps): JSX.Element => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [refetchStartTime, setRefetchStartTime] = useState<number | null>(null);
|
||||
const [currentPolicyShown, setCurrentPolicyShown] = useState<IHostPolicy>();
|
||||
|
|
@ -129,7 +137,6 @@ const WelcomeHost = (): JSX.Element => {
|
|||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__loading`}>
|
||||
<p>Adding your device to Fleet</p>
|
||||
<Spinner />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -137,6 +144,26 @@ const WelcomeHost = (): JSX.Element => {
|
|||
}
|
||||
|
||||
if (loadingHostError) {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__empty-hosts`}>
|
||||
<p>Add your personal device to assess the security of your device.</p>
|
||||
<p>
|
||||
In Fleet, laptops, workstations, and servers are referred to as
|
||||
"hosts."
|
||||
</p>
|
||||
<Button
|
||||
onClick={toggleAddHostsModal}
|
||||
className={`${baseClass}__add-host button button--brand`}
|
||||
>
|
||||
<span>Add hosts</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (totalsHostsCount === 1 && host && host.status === "offline") {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__error`}>
|
||||
|
|
@ -194,7 +221,7 @@ const WelcomeHost = (): JSX.Element => {
|
|||
);
|
||||
}
|
||||
|
||||
if (host) {
|
||||
if (totalsHostsCount === 1 && host && host.status === "online") {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__intro`}>
|
||||
|
|
@ -204,53 +231,48 @@ const WelcomeHost = (): JSX.Element => {
|
|||
{host.hostname}
|
||||
<img alt="" src={LinkArrow} />
|
||||
</Link>
|
||||
<p>
|
||||
Your device is successully connected to this local preview of
|
||||
Fleet.
|
||||
</p>
|
||||
<p>Your host is successfully connected to Fleet.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__blurb`}>
|
||||
<p>
|
||||
Fleet already ran the following checks to assess the security of
|
||||
Fleet already ran the following policies to assess the security of
|
||||
your device:{" "}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__policies`}>
|
||||
{host.policies?.slice(0, 10).map((p) => {
|
||||
{host.policies?.slice(0, 3).map((p) => {
|
||||
if (p.response) {
|
||||
return (
|
||||
<div className="policy-block">
|
||||
<div className="info">
|
||||
<img
|
||||
alt={p.response}
|
||||
src={p.response === policyPass ? IconPassed : IconError}
|
||||
/>
|
||||
{p.name}
|
||||
</div>
|
||||
<Button
|
||||
variant="text-icon"
|
||||
onClick={() => handlePolicyModal(p.id)}
|
||||
>
|
||||
<Button
|
||||
variant="text-icon"
|
||||
onClick={() => handlePolicyModal(p.id)}
|
||||
>
|
||||
<div className="policy-block">
|
||||
<div className="info">
|
||||
<img
|
||||
alt={p.response}
|
||||
src={p.response === policyPass ? IconPassed : IconError}
|
||||
/>
|
||||
{p.name}
|
||||
</div>
|
||||
<img alt="" src={IconChevron} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
{host.policies?.length > 10 && (
|
||||
{host.policies?.length > 3 && (
|
||||
<Link to={PATHS.HOST_DETAILS(host)} className="external-link">
|
||||
Go to Host details to see all checks
|
||||
Go to Host details to see all policies
|
||||
<img alt="" src={LinkArrow} />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${baseClass}__blurb`}>
|
||||
<p>
|
||||
Resolved a failing check? Refetch your device information to verify.
|
||||
</p>
|
||||
<p>Resolved a failing policy? Refetch your host vitals to verify.</p>
|
||||
</div>
|
||||
<div className={`${baseClass}__refetch`}>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -57,14 +57,28 @@
|
|||
margin-top: $pad-large;
|
||||
margin-bottom: $pad-large;
|
||||
|
||||
> * {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
button {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
img {
|
||||
transform: scale(0.58) rotate(-90deg);
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.policy-block {
|
||||
width: 100%;
|
||||
padding: $pad-small 12px;
|
||||
border: 1px solid $ui-fleet-black-25;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
|
||||
.info {
|
||||
font-size: $small;
|
||||
|
|
@ -79,15 +93,11 @@
|
|||
top: -1px;
|
||||
}
|
||||
}
|
||||
button {
|
||||
height: auto;
|
||||
|
||||
img {
|
||||
transform: scale(0.58) rotate(-90deg);
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__loading {
|
||||
.loading-spinner {
|
||||
margin: $pad-medium auto;
|
||||
}
|
||||
}
|
||||
&__refetch {
|
||||
|
|
|
|||
Loading…
Reference in a new issue