New card for preview self host policies (#2666)

This commit is contained in:
Martavis Parker 2021-10-26 12:47:34 -07:00 committed by GitHub
parent 623a38aa9d
commit 9f804e1858
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 627 additions and 82 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1 @@
- Added new cards to home page to display preview mode host and its policies

View file

@ -15,6 +15,7 @@ type InitialStateType = {
currentUser: IUser | null;
currentTeam: ITeam | undefined;
enrollSecret: IEnrollSecret[] | null;
isPreviewMode: boolean | undefined;
isFreeTier: boolean | undefined;
isPremiumTier: boolean | undefined;
isGlobalAdmin: boolean | undefined;
@ -37,6 +38,7 @@ const initialState = {
currentUser: null,
currentTeam: undefined,
enrollSecret: null,
isPreviewMode: false,
isFreeTier: undefined,
isPremiumTier: undefined,
isGlobalAdmin: undefined,
@ -61,6 +63,10 @@ const actions = {
SET_ENROLL_SECRET: "SET_ENROLL_SECRET",
};
const detectPreview = () => {
return window.location.origin === "http://localhost:1337";
};
// helper function - this is run every
// time currentUser, config, or teamId is changed
const setPermissions = (user: IUser, config: IConfig, teamId = 0) => {
@ -125,6 +131,7 @@ const AppProvider = ({ children }: Props) => {
currentUser: state.currentUser,
currentTeam: state.currentTeam,
enrollSecret: state.enrollSecret,
isPreviewMode: detectPreview(),
isFreeTier: state.isFreeTier,
isPremiumTier: state.isPremiumTier,
isGlobalAdmin: state.isGlobalAdmin,

View file

@ -11,10 +11,13 @@ import { ISoftware } from "interfaces/software";
import TeamsDropdown from "components/TeamsDropdown";
import Button from "components/buttons/Button";
import HostsSummary from "./HostsSummary";
import ActivityFeed from "./ActivityFeed";
import InfoCard from "./components/InfoCard";
import HostsSummary from "./cards/HostsSummary";
import ActivityFeed from "./cards/ActivityFeed";
import Software from "./cards/Software";
import LearnFleet from "./cards/LearnFleet";
import WelcomeHost from "./cards/WelcomeHost";
import LinkArrow from "../../../assets/images/icon-arrow-right-vibrant-blue-10x18@2x.png";
import Software from "./Software";
interface ITeamsResponse {
teams: ITeam[];
@ -24,9 +27,13 @@ const baseClass = "homepage";
const Homepage = (): JSX.Element => {
const { MANAGE_HOSTS } = paths;
const { config, currentTeam, isPremiumTier, setCurrentTeam } = useContext(
AppContext
);
const {
config,
currentTeam,
isPremiumTier,
isPreviewMode,
setCurrentTeam,
} = useContext(AppContext);
const [isSoftwareModalOpen, setIsSoftwareModalOpen] = useState<boolean>(
false
@ -46,8 +53,6 @@ const Homepage = (): JSX.Element => {
setCurrentTeam(selectedTeam);
};
const canSeeActivity = !isPremiumTier || !currentTeam;
return (
<div className={baseClass}>
<div className={`${baseClass}__header-wrap`}>
@ -69,25 +74,28 @@ const Homepage = (): JSX.Element => {
</div>
</div>
<div className={`${baseClass}__section one-column`}>
<div className={`${baseClass}__info-card`}>
<div className={`${baseClass}__section-title`}>
<h2>Hosts</h2>
<Link to={MANAGE_HOSTS} className={`${baseClass}__action-button`}>
<span>View all hosts</span>
<img src={LinkArrow} alt="link arrow" id="link-arrow" />
</Link>
</div>
<InfoCard
title="Hosts"
action={{ type: "link", to: MANAGE_HOSTS, text: "View all hosts" }}
>
<HostsSummary />
</div>
</InfoCard>
</div>
{canSeeActivity && (
{isPreviewMode && (
<div className={`${baseClass}__section two-column`}>
<InfoCard title="Welcome to Fleet">
<WelcomeHost />
</InfoCard>
<InfoCard title="Learn how to use Fleet">
<LearnFleet />
</InfoCard>
</div>
)}
{!isPreviewMode && !currentTeam && (
<div className={`${baseClass}__section one-column`}>
<div className={`${baseClass}__info-card`}>
<div className={`${baseClass}__section-title`}>
<h2>Activity</h2>
</div>
<InfoCard title="Activity">
<ActivityFeed />
</div>
</InfoCard>
</div>
)}
{/* TODO: Re-add this commented out section once the /software API is running */}
@ -96,32 +104,23 @@ const Homepage = (): JSX.Element => {
${currentTeam ? 'one' : 'two'}-column
`}>
{!currentTeam && (
<div className={`${baseClass}__info-card`}>
<div className={`${baseClass}__section-title`}>
<h2>Software</h2>
<Button
className={`${baseClass}__action-button`}
variant="text-link"
onClick={() => setIsSoftwareModalOpen(true)}
>
<>
<span>View all software</span>
<img src={LinkArrow} alt="link arrow" id="link-arrow" />
</>
</Button>
</div>
<InfoCard
title="Software"
action={{
type: button,
text: "View all software",
onClick: () => setIsSoftwareModalOpen(true)
}}
>
<Software
isModalOpen={isSoftwareModalOpen}
setIsSoftwareModalOpen={setIsSoftwareModalOpen}
/>
</div>
</InfoCard>
)}
<div className={`${baseClass}__info-card`}>
<div className={`${baseClass}__section-title`}>
<h2>Activity</h2>
</div>
<InfoCard title="Activity">
<ActivityFeed />
</div>
</InfoCard>
</div> */}
</div>
);

View file

@ -12,19 +12,6 @@
margin: 0;
}
&__action-button {
display: flex;
align-items: center;
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none !important;
}
#link-arrow {
transform: scale(0.5);
}
&__header-wrap {
display: flex;
align-items: center;
@ -78,19 +65,4 @@
}
}
}
&__info-card {
padding: 32px;
width: 100%;
background-color: $core-white;
border: 1px solid $ui-fleet-blue-15;
border-radius: 8px;
box-shadow: 0 2px 0 0 $ui-fleet-blue-15;
box-sizing: border-box;
}
&__section-title {
display: flex;
justify-content: space-between;
}
}

View file

@ -14,8 +14,8 @@ import Button from "components/buttons/Button";
import Spinner from "components/loaders/Spinner"; // @ts-ignore
import FleetIcon from "components/icons/FleetIcon";
import ErrorIcon from "../../../../assets/images/icon-error-16x16@2x.png";
import OpenNewTabIcon from "../../../../assets/images/open-new-tab-12x12@2x.png";
import ErrorIcon from "../../../../../assets/images/icon-error-16x16@2x.png";
import OpenNewTabIcon from "../../../../../assets/images/open-new-tab-12x12@2x.png";
const baseClass = "activity-feed";

View file

@ -4,9 +4,9 @@ import { reduce } from "lodash";
import { ILabel } from "interfaces/label";
// @ts-ignore
import { getLabels } from "redux/nodes/components/ManageHostsPage/actions";
import WindowsIcon from "../../../../assets/images/icon-windows-48x48@2x.png";
import LinuxIcon from "../../../../assets/images/icon-linux-48x48@2x.png";
import MacIcon from "../../../../assets/images/icon-mac-48x48@2x.png";
import WindowsIcon from "../../../../../assets/images/icon-windows-48x48@2x.png";
import LinuxIcon from "../../../../../assets/images/icon-linux-48x48@2x.png";
import MacIcon from "../../../../../assets/images/icon-mac-48x48@2x.png";
const baseClass = "hosts-summary";

View file

@ -0,0 +1,27 @@
import React from "react";
import LinkArrow from "../../../../../assets/images/icon-arrow-right-vibrant-blue-10x18@2x.png";
const baseClass = "learn-fleet";
const LearnFleet = (): JSX.Element => {
return (
<div className={baseClass}>
<p>
Want to explore Fleet&apos;s features? Learn how to ask questions about
your device using queries.
</p>
<a
target="_blank"
rel="noreferrer noopener"
className="homepage-info-card__action-button"
href="https://fleetdm.com/docs/using-fleet/learn-how-to-use-fleet"
>
Learn how to use Fleet
<img src={LinkArrow} alt="link arrow" id="link-arrow" />
</a>
</div>
);
};
export default LearnFleet;

View file

@ -0,0 +1,5 @@
.learn-fleet {
p {
font-size: $x-small;
}
}

View file

@ -0,0 +1 @@
export { default } from "./LearnFleet";

View file

@ -6,7 +6,7 @@ import { ISoftware } from "interfaces/software";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import Chevron from "../../../../assets/images/icon-chevron-blue-16x16@2x.png";
import Chevron from "../../../../../assets/images/icon-chevron-blue-16x16@2x.png";
interface IHeaderProps {
column: {

View file

@ -0,0 +1,293 @@
import React, { useState } from "react";
import PATHS from "router/paths";
import { Link } from "react-router";
import { useQuery } from "react-query";
import { useDispatch } from "react-redux";
import moment from "moment";
import { IHost } from "interfaces/host";
import { IHostPolicy } from "interfaces/host_policy";
import hostAPI from "services/entities/hosts"; // @ts-ignore
import { renderFlash } from "redux/nodes/notifications/actions";
import Spinner from "components/loaders/Spinner";
import Button from "components/buttons/Button";
import Modal from "components/modals/Modal";
import LaptopMac from "../../../../../assets/images/laptop-mac.png";
import LinkArrow from "../../../../../assets/images/icon-arrow-right-vibrant-blue-10x18@2x.png";
import IconDisabled from "../../../../../assets/images/icon-action-disable-red-16x16@2x.png";
import IconPassed from "../../../../../assets/images/icon-check-circle-green-16x16@2x.png";
import IconError from "../../../../../assets/images/icon-exclamation-circle-red-16x16@2x.png";
import IconChevron from "../../../../../assets/images/icon-chevron-purple-9x6@2x.png";
import SlackButton from "../../../../../assets/images/slack-button-get-help.png";
interface IHostResponse {
host: IHost;
}
const baseClass = "welcome-host";
const HOST_ID = 8;
const WelcomeHost = (): JSX.Element => {
const dispatch = useDispatch();
const [refetchStartTime, setRefetchStartTime] = useState<number | null>(null);
const [currentPolicyShown, setCurrentPolicyShown] = useState<IHostPolicy>();
const [showPolicyModal, setShowPolicyModal] = useState<boolean>(false);
const [
showRefetchLoadingSpinner,
setShowRefetchLoadingSpinner,
] = useState<boolean>(false);
const {
isLoading: isLoadingHost,
data: host,
error: loadingHostError,
refetch: fullyReloadHost,
} = useQuery<IHostResponse, Error, IHost>(
["host"],
() => hostAPI.load(HOST_ID),
{
select: (data: IHostResponse) => data.host,
onSuccess: (returnedHost) => {
setShowRefetchLoadingSpinner(returnedHost.refetch_requested);
if (returnedHost.refetch_requested) {
// Code duplicated from HostDetailsPage. See comments there.
if (!refetchStartTime) {
if (returnedHost.status === "online") {
setRefetchStartTime(Date.now());
setTimeout(() => {
fullyReloadHost();
}, 1000);
} else {
setShowRefetchLoadingSpinner(false);
}
} else {
const totalElapsedTime = Date.now() - refetchStartTime;
if (totalElapsedTime < 60000) {
if (returnedHost.status === "online") {
setTimeout(() => {
fullyReloadHost();
}, 1000);
} else {
dispatch(
renderFlash(
"error",
`This host is offline. Please try refetching host vitals later.`
)
);
setShowRefetchLoadingSpinner(false);
}
} else {
dispatch(
renderFlash(
"error",
`We're having trouble fetching fresh vitals for this host. Please try again later.`
)
);
setShowRefetchLoadingSpinner(false);
}
}
}
},
onError: (error) => {
console.log(error);
dispatch(
renderFlash("error", `Unable to load host. Please try again.`)
);
},
}
);
const onRefetchHost = async () => {
if (host) {
setShowRefetchLoadingSpinner(true);
try {
await hostAPI.refetch(host).then(() => {
setRefetchStartTime(Date.now());
setTimeout(() => fullyReloadHost(), 1000);
});
} catch (error) {
console.log(error);
dispatch(renderFlash("error", `Host "${host.hostname}" refetch error`));
setShowRefetchLoadingSpinner(false);
}
}
};
const handlePolicyModal = (id: number) => {
const policy = host?.policies.find((p) => p.id === id);
if (policy) {
setCurrentPolicyShown(policy);
setShowPolicyModal(true);
}
};
if (isLoadingHost) {
return (
<div className={baseClass}>
<div className={`${baseClass}__loading`}>
<p>Adding your device to Fleet</p>
<Spinner />
</div>
</div>
);
}
if (loadingHostError) {
return (
<div className={baseClass}>
<div className={`${baseClass}__error`}>
<p>
<img
alt="Disabled icon"
className="icon-disabled"
src={IconDisabled}
/>
Your device is not communicating with Fleet.
</p>
<p>Join the #fleet Slack channel for help troubleshooting.</p>
<a
target="_blank"
rel="noreferrer"
href="https://osquery.slack.com/archives/C01DXJL16D8"
>
<img
alt="Get help on Slack"
className="button-slack"
src={SlackButton}
/>
</a>
</div>
</div>
);
}
if (host && !host.policies) {
return (
<div className={baseClass}>
<div className={`${baseClass}__error`}>
<p className="error-message">
<img
alt="Disabled icon"
className="icon-disabled"
src={IconDisabled}
/>
No policies apply to your device.
</p>
<p>Join the #fleet Slack channel for help troubleshooting.</p>
<a
target="_blank"
rel="noreferrer"
href="https://osquery.slack.com/archives/C01DXJL16D8"
>
<img
alt="Get help on Slack"
className="button-slack"
src={SlackButton}
/>
</a>
</div>
</div>
);
}
if (host) {
return (
<div className={baseClass}>
<div className={`${baseClass}__intro`}>
<img alt="" src={LaptopMac} />
<div className="info">
<Link to={PATHS.HOST_DETAILS(host)} className="external-link">
{host.hostname}
<img alt="" src={LinkArrow} />
</Link>
<p>
Your device is successully connected to this local preview of
Fleet.
</p>
</div>
</div>
<div className={`${baseClass}__blurb`}>
<p>
Fleet already ran the following checks to assess the security of
your device:{" "}
</p>
</div>
<div className={`${baseClass}__policies`}>
{host.policies?.slice(0, 10).map((p) => (
<div className="policy-block">
<div className="info">
<img
alt={p.response}
src={p.response === "passing" ? IconPassed : IconError}
/>
{p.query_name}
</div>
<Button
variant="text-icon"
onClick={() => handlePolicyModal(p.id)}
>
<img alt="" src={IconChevron} />
</Button>
</div>
))}
{host.policies?.length > 10 && (
<Link to={PATHS.HOST_DETAILS(host)} className="external-link">
Go to Host details to see all checks
<img alt="" src={LinkArrow} />
</Link>
)}
</div>
<div className={`${baseClass}__blurb`}>
<p>
Resolved a failing check? Refetch your device information to verify.
</p>
</div>
<div className={`${baseClass}__refetch`}>
<Button
variant="blue-green"
className={`refetch-spinner ${
showRefetchLoadingSpinner ? "spin" : ""
}`}
onClick={onRefetchHost}
disabled={showRefetchLoadingSpinner}
>
Refetch
</Button>
<span>Last updated {moment(host.detail_updated_at).fromNow()}</span>
</div>
{showPolicyModal && (
<Modal
title={currentPolicyShown?.query_name || ""}
onExit={() => setShowPolicyModal(false)}
className={`${baseClass}__policy-modal`}
>
<>
<p>{currentPolicyShown?.query_description}</p>
{currentPolicyShown?.resolution && (
<p>
<b>Resolve:</b>
{currentPolicyShown.resolution}
</p>
)}
<div className="done">
<Button
variant="brand"
onClick={() => setShowPolicyModal(false)}
>
Done
</Button>
</div>
</>
</Modal>
)}
</div>
);
}
return <Spinner />;
};
export default WelcomeHost;

View file

@ -0,0 +1,141 @@
.welcome-host {
p {
font-size: $x-small;
}
.button-slack {
width: 173px;
margin-top: 4px;
}
.icon-disabled {
width: 16px;
margin-right: 8px;
position: relative;
top: 3px;
}
p.error-message {
font-weight: $bold;
margin: 1.5rem 0;
}
.external-link {
display: flex;
align-items: center;
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none !important;
img {
transform: scale(0.5);
}
&:last-of-type {
margin-top: $pad-large;
}
}
&__intro {
margin-top: $pad-large;
display: flex;
align-items: center;
& > img {
width: 126px;
}
.info {
margin-left: $pad-medium;
flex: 1;
p {
margin: 0;
margin-top: $pad-small;
}
}
}
&__policies {
margin-top: $pad-large;
margin-bottom: $pad-large;
.policy-block {
padding: $pad-small 12px;
border: 1px solid $ui-fleet-black-25;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
.info {
font-size: $small;
display: flex;
align-items: center;
line-height: 1;
img {
margin-right: $pad-large;
transform: scale(0.5);
position: relative;
top: -1px;
}
}
button {
height: auto;
img {
transform: scale(0.58) rotate(-90deg);
position: relative;
top: 1px;
}
}
}
}
&__refetch {
display: flex;
align-items: center;
button {
margin-right: $pad-small;
&.refetch-spinner {
color: $core-white;
font-size: $x-small;
height: 38px;
&::before {
display: inline-block;
position: relative;
padding: 5px 0 0 0; // centers spin
display: inline-block;
content: url(../assets/images/icon-refetch-white-12x12@2x.png);
transform: scale(0.5);
}
&.spin::before {
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: scale(0.5) rotate(0deg);
transform-origin: center center;
}
100% {
transform: scale(0.5) rotate(360deg);
transform-origin: center center;
}
}
}
}
span {
padding-left: 4px;
font-size: $x-small;
}
}
&__policy-modal {
.done {
margin-top: $pad-large;
display: flex;
justify-content: flex-end;
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./WelcomeHost";

View file

@ -0,0 +1,65 @@
import React from "react";
import { Link } from "react-router";
import Button from "components/buttons/Button";
import LinkArrow from "../../../../../assets/images/icon-arrow-right-vibrant-blue-10x18@2x.png";
interface IInfoCardProps {
title: string;
children: React.ReactChild | React.ReactChild[];
action?:
| {
type: "link";
to: string;
text: string;
}
| {
type: "button";
text: string;
onClick?: () => void;
};
}
const baseClass = "homepage-info-card";
const InfoCard = ({ title, children, action }: IInfoCardProps) => {
const renderAction = () => {
if (action) {
if (action.type === "button") {
return (
<Button
className={`${baseClass}__action-button`}
variant="text-link"
onClick={action.onClick}
>
<>
<span>{action.text}</span>
<img src={LinkArrow} alt="link arrow" id="link-arrow" />
</>
</Button>
);
}
return (
<Link to={action.to} className={`${baseClass}__action-button`}>
<span>{action.text}</span>
<img src={LinkArrow} alt="link arrow" id="link-arrow" />
</Link>
);
}
return null;
};
return (
<div className={baseClass}>
<div className={`${baseClass}__section-title`}>
<h2>{title}</h2>
{renderAction()}
</div>
{children}
</div>
);
};
export default InfoCard;

View file

@ -0,0 +1,31 @@
.homepage-info-card {
padding: 32px;
width: 100%;
background-color: $core-white;
border: 1px solid $ui-fleet-blue-15;
border-radius: 8px;
box-shadow: 0 2px 0 0 $ui-fleet-blue-15;
box-sizing: border-box;
&__section-title {
display: flex;
justify-content: space-between;
h2 {
font-weight: $bold;
}
}
&__action-button {
display: flex;
align-items: center;
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none !important;
}
#link-arrow {
transform: scale(0.5);
}
}

View file

@ -0,0 +1 @@
export { default } from "./InfoCard";

View file

@ -20,7 +20,7 @@ interface ILoginData {
const PreviewLoginPage = () => {
const dispatch = useDispatch();
const { setCurrentUser } = useContext(AppContext);
const { isPreviewMode, setCurrentUser } = useContext(AppContext);
const [loginVisible, setLoginVisible] = useState<boolean>(true);
const onSubmit = debounce((formData: ILoginData) => {
@ -41,7 +41,7 @@ const PreviewLoginPage = () => {
});
useEffect(() => {
if (window.location.origin === "http://localhost:1337") {
if (isPreviewMode) {
onSubmit({
email: "admin@example.com",
password: "admin123#",

View file

@ -1,8 +1,8 @@
import React, { useState } from "react";
import React, { useContext, useState } from "react";
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import FileSaver from "file-saver";
// @ts-ignore
import { AppContext } from "context/app"; // @ts-ignore
import Fleet from "fleet"; // @ts-ignore
import { stringToClipboard } from "utilities/copy_text";
import { ITeam } from "interfaces/team";
@ -49,6 +49,7 @@ const PlatformWrapper = ({
selectedTeam,
onCancel,
}: IPlatformWrapperProp): JSX.Element => {
const { config } = useContext(AppContext);
const [copyMessage, setCopyMessage] = useState<string>("");
const [certificate, setCertificate] = useState<string | undefined>(undefined);
const [fetchCertificateError, setFetchCertificateError] = useState<
@ -86,7 +87,7 @@ const PlatformWrapper = ({
enrollSecret = selectedTeam.secrets[0].secret;
}
let installerString = `fleetctl package --type=${platform} --fleet-url=https://localhost:8412 --enroll-secret=${enrollSecret}`;
let installerString = `fleetctl package --type=${platform} --fleet-url=${config?.server_url} --enroll-secret=${enrollSecret}`;
if (platform === "rpm" || platform === "deb") {
installerString +=
" --fleet-certificate=/home/username/Downloads/fleet.pem";