Homepage Dashboard - New feature! (#1340)

* Adds homepage dashboard to Fleet app
* Host Summary is displayed on the dashboard
This commit is contained in:
RachelElysia 2021-07-12 10:15:47 -07:00 committed by GitHub
parent 2bb2bf2d5d
commit 29e900d7c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 393 additions and 68 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,2 @@
* Adds homepage to Fleet app
* Host Summary is displayed on Homepage

View File

@ -1,31 +0,0 @@
import React, { Component } from "react";
import configInterface from "interfaces/config";
import OrgLogoIcon from "components/icons/OrgLogoIcon";
class SiteNavHeader extends Component {
static propTypes = {
config: configInterface,
};
render() {
const {
config: { org_logo_url: orgLogoURL },
} = this.props;
const headerBaseClass = "site-nav-header";
return (
<header className={headerBaseClass}>
<div className={`${headerBaseClass}__inner`}>
<OrgLogoIcon
className={`${headerBaseClass}__logo`}
src={orgLogoURL}
/>
</div>
</header>
);
}
}
export default SiteNavHeader;

View File

@ -1,28 +0,0 @@
.site-nav-header {
position: relative;
padding: 14px 20px;
&__inner {
display: flex;
align-items: center;
}
&__logo {
@include size(24px);
border-radius: 20%;
}
&__username {
@include ellipsis(110px);
margin: 0;
padding: 0;
color: $core-white;
font-size: $small;
font-weight: $bold;
position: relative;
@include breakpoint(smalldesk) {
display: none;
}
}
}

View File

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

View File

@ -3,7 +3,9 @@ import PropTypes from "prop-types";
import classnames from "classnames";
import userInterface from "interfaces/user";
import configInterface from "interfaces/config";
import UserMenu from "components/side_panels/UserMenu";
import OrgLogoIcon from "components/icons/OrgLogoIcon";
import navItems from "./navItems";
@ -18,6 +20,7 @@ class SiteNavSidePanel extends Component {
onNavItemClick: PropTypes.func,
pathname: PropTypes.string,
user: userInterface,
config: configInterface,
};
constructor(props) {
@ -30,7 +33,11 @@ class SiteNavSidePanel extends Component {
renderNavItem = (navItem) => {
const { name, iconName } = navItem;
const { onNavItemClick, pathname } = this.props;
const {
onNavItemClick,
pathname,
config: { org_logo_url: orgLogoURL },
} = this.props;
const active = navItem.location.regex.test(pathname);
const navItemBaseClass = "site-nav-item";
@ -60,6 +67,18 @@ class SiteNavSidePanel extends Component {
<img src={AdminIcon} alt={`${iconName} icon`} className={iconClasses} />
);
if (iconName === "logo") {
return (
<li className={navItemClasses} key={`nav-item-${name}`}>
<a
className={`${navItemBaseClass}__link`}
onClick={onNavItemClick(navItem.location.pathname)}
>
<OrgLogoIcon className="logo" src={orgLogoURL} />
</a>
</li>
);
}
return (
<li className={navItemClasses} key={`nav-item-${name}`}>
<a
@ -76,6 +95,7 @@ class SiteNavSidePanel extends Component {
renderNavItems = () => {
const { renderNavItem, userNavItems } = this;
const { onLogoutUser, user, onNavItemClick } = this.props;
return (
<div className="site-nav-container">
<ul className="site-nav-list">

View File

@ -3,6 +3,7 @@
transition: color 200ms ease-in-out;
cursor: pointer;
border-bottom: 3px solid transparent;
max-height: 50px;
&:hover {
background-color: $core-fleet-black;
@ -31,6 +32,13 @@
vertical-align: sub;
}
.logo {
@include size(48px);
border-radius: 20%;
padding: 0;
margin: -12px -15px 0;
}
&__name {
text-decoration: none;
vertical-align: middle;
@ -60,6 +68,10 @@
}
}
.logo {
transform: scale(0.5);
}
.site-nav-container {
flex-grow: 1;
display: flex;

View File

@ -16,6 +16,15 @@ export default (currentUser) => {
];
const userNavItems = [
{
icon: "logo",
name: "Home",
iconName: "logo",
location: {
regex: new RegExp(`^${URL_PREFIX}/home/dashboard`),
pathname: PATHS.HOMEPAGE,
},
},
{
icon: "hosts",
name: "Hosts",

View File

@ -7,7 +7,6 @@ import { push } from "react-router-redux";
import configInterface from "interfaces/config";
import FlashMessage from "components/flash_messages/FlashMessage";
import PersistentFlash from "components/flash_messages/PersistentFlash";
import SiteNavHeader from "components/side_panels/SiteNavHeader";
import SiteNavSidePanel from "components/side_panels/SiteNavSidePanel";
import userInterface from "interfaces/user";
import notificationInterface from "interfaces/notification";
@ -93,12 +92,6 @@ export class CoreLayout extends Component {
return (
<div className="app-wrap">
<nav className="site-nav">
<SiteNavHeader
config={config}
onLogoutUser={onLogoutUser}
onNavItemClick={onNavItemClick}
user={user}
/>
<SiteNavSidePanel
config={config}
onLogoutUser={onLogoutUser}

View File

@ -0,0 +1,59 @@
import React from "react";
import { useSelector } from "react-redux";
import paths from "router/paths";
import { Link } from "react-router";
import { IUser } from "interfaces/user";
import HostsSummary from "./HostsSummary";
import LinkArrow from "../../../../assets/images/icon-arrow-right-vibrant-blue-10x18@2x.png";
const baseClass = "dashboard";
interface IRootState {
auth: {
user: IUser;
};
app: {
config: {
org_name: string;
};
};
}
const Dashboard = (): JSX.Element => {
const { MANAGE_HOSTS } = paths;
const user = useSelector((state: IRootState) => state.auth.user);
const orgName = useSelector((state: IRootState) => state.app.config.org_name);
return (
<div className={baseClass}>
<div className={`${baseClass}__wrapper body-wrap`}>
<div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__header`}>
<h1 className={`${baseClass}__title`}>
<span>{orgName}</span>
</h1>
</div>
</div>
<div className={`${baseClass}__section hosts-section`}>
<div className={`${baseClass}__section-title`}>
<div>
<h2>Hosts</h2>
</div>
<Link to={MANAGE_HOSTS} className={`${baseClass}__host-link`}>
<span>View all hosts</span>
<img src={LinkArrow} alt="link arrow" id="link-arrow" />
</Link>
</div>
<div className={`${baseClass}__section-details`}>
<HostsSummary />
</div>
</div>
</div>
</div>
);
};
export default Dashboard;

View File

@ -0,0 +1,79 @@
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
// @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";
const baseClass = "hosts-summary";
interface IRootState {
entities: {
labels: {
isLoading: boolean;
data: {
[id: number]: {
count: number;
};
};
};
};
}
const HostsSummary = (): JSX.Element => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getLabels());
}, []);
const labels = useSelector((state: IRootState) => state.entities.labels.data);
const macCount = labels[7] ? labels[7].count.toLocaleString("en-US") : "";
const windowsCount = labels[10]
? labels[10].count.toLocaleString("en-US")
: "";
const linuxCount =
labels[8] && labels[9]
? (labels[8].count + labels[9].count + labels[11].count).toLocaleString(
"en-US"
)
: "";
return (
<div className={baseClass}>
<div className={`${baseClass}__tiles`}>
<div className={`${baseClass}__tile mac-tile`}>
<div className={`${baseClass}__tile-icon`}>
<img src={MacIcon} alt="mac icon" id="mac-icon" />
</div>
<div className={`${baseClass}__tile-count mac-count`}>{macCount}</div>
<div className={`${baseClass}__tile-description`}>macOS hosts</div>
</div>
<div className={`${baseClass}__tile windows-tile`}>
<div className={`${baseClass}__tile-icon`}>
<img src={WindowsIcon} alt="windows icon" id="windows-icon" />
</div>
<div className={`${baseClass}__tile-count windows-count`}>
{windowsCount}
</div>
<div className={`${baseClass}__tile-description`}>Windows hosts</div>
</div>
<div className={`${baseClass}__tile linux-tile`}>
<div className={`${baseClass}__tile-icon`}>
<img src={LinuxIcon} alt="linux icon" id="linux-icon" />
</div>
<div className={`${baseClass}__tile-count linux-count`}>
{linuxCount}
</div>
<div className={`${baseClass}__tile-description`}>Linux hosts</div>
</div>
</div>
</div>
);
};
export default HostsSummary;

View File

@ -0,0 +1,42 @@
.hosts-summary {
align-self: center;
width: 100%;
max-width: 900px;
&__tiles {
display: flex;
flex-direction: row;
justify-content: space-between;
}
&__tile {
background-color: $ui-off-white;
height: 180px;
flex-grow: 1;
margin-right: 16px;
border: solid 1px $ui-fleet-blue-15;
border-radius: 4px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&:last-child {
margin-right: 0;
}
}
&__tile-icon {
margin: -15px;
}
&__tile-count {
font-size: $large;
}
#mac-icon,
#windows-icon,
#linux-icon {
transform: scale(0.5);
}
}

View File

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

View File

@ -0,0 +1,144 @@
.dashboard {
align-self: center;
width: 100%;
max-width: 900px;
h2 {
font-size: $small;
font-weight: $regular;
margin: 0;
}
a {
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none;
}
#link-arrow {
transform: scale(0.5);
margin-bottom: -4px;
}
&__body-wrap {
align-items: center;
}
&__header-wrap {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $pad-large;
}
&__header {
display: flex;
align-items: center;
}
&__title {
font-size: $large;
.fleeticon {
color: $core-fleet-blue;
margin-right: 15px;
}
.fleeticon-success-check {
color: $ui-success;
}
.fleeticon-offline {
color: $ui-error;
}
.fleeticon-mia {
color: $core-fleet-black;
}
}
&__section {
width: 95%;
max-width: 800px;
margin: auto;
margin-bottom: $pad-xlarge;
}
&__section-title {
display: flex;
justify-content: space-between;
padding-bottom: $pad-small;
border-bottom: solid 1px $ui-fleet-blue-15;
margin-bottom: $pad-large;
}
&__section-details {
font-size: $x-small;
}
// For future use
// .ace-fleet {
// margin-bottom: $pad-medium;
// }
// &__label-actions {
// button {
// &:first-child {
// margin-right: $pad-medium;
// }
// }
// }
// &__description {
// margin: 0 0 $pad-medium;
// h2 {
// text-transform: uppercase;
// color: $core-fleet-black;
// font-weight: $regular;
// font-size: $small;
// }
// p {
// color: $core-fleet-blue;
// margin: 0;
// font-size: $x-small;
// font-style: italic;
// }
// }
// &__toggle-view {
// .fleeticon {
// width: 24px;
// height: 24px;
// margin-left: 12px;
// fill: $ui-gray;
// }
// &--active {
// .fleeticon {
// fill: $core-fleet-purple;
// }
// }
// }
// .data-table-container {
// .data-table {
// &__wrapper {
// overflow-x: scroll;
// }
// }
// }
// &__modal-buttons {
// width: 100%;
// display: flex;
// justify-content: flex-end;
// .button:first-child {
// margin-right: $pad-medium;
// }
// }
}

View File

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

View File

@ -0,0 +1,16 @@
import React from "react";
import PropTypes from "prop-types";
class Homepage extends React.Component {
static propTypes = {
children: PropTypes.node,
};
render() {
const { children } = this.props;
return children || null;
}
}
export default Homepage;

View File

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

View File

@ -26,6 +26,8 @@ import CoreLayout from "layouts/CoreLayout";
import EditPackPage from "pages/packs/EditPackPage";
import EmailTokenRedirect from "components/EmailTokenRedirect";
import HostDetailsPage from "pages/hosts/HostDetailsPage";
import Homepage from "pages/Homepage";
import Dashboard from "pages/Homepage/Dashboard";
import LoginRoutes from "components/LoginRoutes";
import LogoutPage from "pages/LogoutPage";
import ManageHostsPage from "pages/hosts/ManageHostsPage";
@ -67,6 +69,9 @@ const routes = (
<Route path="logout" component={LogoutPage} />
<Route component={CoreLayout}>
<IndexRedirect to={PATHS.MANAGE_HOSTS} />
<Route path="home" component={Homepage}>
<Route path="dashboard" component={Dashboard} />
</Route>
<Route path="settings" component={AuthenticatedAdminRoutes}>
<Route component={SettingsWrapper}>
<Route path="organization" component={AdminAppSettingsPage} />

View File

@ -23,6 +23,7 @@ export default {
FLEET_403: `${URL_PREFIX}/403`,
FLEET_500: `${URL_PREFIX}/500`,
LOGIN: `${URL_PREFIX}/login`,
HOMEPAGE: `${URL_PREFIX}/home/dashboard`,
LOGOUT: `${URL_PREFIX}/logout`,
MANAGE_HOSTS: `${URL_PREFIX}/hosts/manage`,
HOST_DETAILS: (host: IHost): string => {