mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
Homepage Dashboard - New feature! (#1340)
* Adds homepage dashboard to Fleet app * Host Summary is displayed on the dashboard
This commit is contained in:
parent
2bb2bf2d5d
commit
29e900d7c3
BIN
assets/images/icon-arrow-right-vibrant-blue-10x18@2x.png
Normal file
BIN
assets/images/icon-arrow-right-vibrant-blue-10x18@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 343 B |
BIN
assets/images/icon-linux-48x48@2x.png
Normal file
BIN
assets/images/icon-linux-48x48@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
BIN
assets/images/icon-mac-48x48@2x.png
Normal file
BIN
assets/images/icon-mac-48x48@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
assets/images/icon-windows-48x48@2x.png
Normal file
BIN
assets/images/icon-windows-48x48@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
2
changes/issue-1319-home-page-host-summary
Normal file
2
changes/issue-1319-home-page-host-summary
Normal file
@ -0,0 +1,2 @@
|
||||
* Adds homepage to Fleet app
|
||||
* Host Summary is displayed on Homepage
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { default } from "./SiteNavHeader";
|
@ -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">
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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}
|
||||
|
59
frontend/pages/Homepage/Dashboard/Dashboard.tsx
Normal file
59
frontend/pages/Homepage/Dashboard/Dashboard.tsx
Normal 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;
|
@ -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;
|
42
frontend/pages/Homepage/Dashboard/HostsSummary/_styles.scss
Normal file
42
frontend/pages/Homepage/Dashboard/HostsSummary/_styles.scss
Normal 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);
|
||||
}
|
||||
}
|
1
frontend/pages/Homepage/Dashboard/HostsSummary/index.tsx
Normal file
1
frontend/pages/Homepage/Dashboard/HostsSummary/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./HostsSummary";
|
144
frontend/pages/Homepage/Dashboard/_styles.scss
Normal file
144
frontend/pages/Homepage/Dashboard/_styles.scss
Normal 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;
|
||||
// }
|
||||
// }
|
||||
}
|
1
frontend/pages/Homepage/Dashboard/index.tsx
Normal file
1
frontend/pages/Homepage/Dashboard/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./Dashboard";
|
16
frontend/pages/Homepage/Homepage.jsx
Normal file
16
frontend/pages/Homepage/Homepage.jsx
Normal 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;
|
1
frontend/pages/Homepage/index.jsx
Normal file
1
frontend/pages/Homepage/index.jsx
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./Homepage";
|
@ -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} />
|
||||
|
@ -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 => {
|
||||
|
Loading…
Reference in New Issue
Block a user