Host details page: packs accordion (#724)

* Pack queries accordion
* Connects to query_stats API
* No packs edge case

Co-authored-by: Sarah Gillespie <sarahgillespi314@gmail.com>
This commit is contained in:
RachelElysia
2021-05-07 18:18:52 -04:00
committed by GitHub
co-authored by Sarah Gillespie
parent 02973ec4a2
commit d4147e916d
12 changed files with 205 additions and 15 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

+4
View File
@@ -5,6 +5,8 @@ export default PropTypes.shape({
name: PropTypes.string,
query: PropTypes.string,
id: PropTypes.number,
interval: PropTypes.number,
last_excuted: PropTypes.string,
});
export interface IQuery {
@@ -12,4 +14,6 @@ export interface IQuery {
name: string;
query: string;
id: number;
interval: number;
last_excuted: string;
}
+11
View File
@@ -301,6 +301,16 @@ export const humanHostDetailUpdated = (detailUpdated: string): string => {
return moment(detailUpdated).fromNow();
};
export const humanQueryLastRun = (lastRun: string): string => {
// Handles the case when a query has never been ran.
// July 28, 2016 is the date of the initial commit to kolide/fleet.
if (lastRun < "2016-07-28T00:00:00Z") {
return "Never";
}
return moment(lastRun).fromNow();
};
export default {
addGravatarUrlToResource,
formatConfigDataForServer,
@@ -313,6 +323,7 @@ export default {
humanHostEnrolled,
humanHostMemory,
humanHostDetailUpdated,
humanQueryLastRun,
labelSlug,
setupData,
};
@@ -11,6 +11,7 @@ import Spinner from "components/loaders/Spinner";
import Button from "components/buttons/Button";
import Modal from "components/modals/Modal";
import SoftwareListRow from "pages/hosts/HostDetailsPage/SoftwareListRow";
import PackQueriesListRow from "pages/hosts/HostDetailsPage/PackQueriesListRow";
import entityGetter from "redux/utilities/entityGetter";
import queryActions from "redux/nodes/entities/queries/actions";
@@ -18,6 +19,13 @@ import queryInterface from "interfaces/query";
import { renderFlash } from "redux/nodes/notifications/actions";
import { push } from "react-router-redux";
import PATHS from "router/paths";
import {
Accordion,
AccordionItem,
AccordionItemHeading,
AccordionItemButton,
AccordionItemPanel,
} from "react-accessible-accordion";
import hostInterface from "interfaces/host";
import {
@@ -234,21 +242,50 @@ export class HostDetailsPage extends Component {
};
renderPacks = () => {
const { onPackClick } = this;
const { host } = this.props;
const { packs = [] } = host;
const { packs = [], pack_stats = [] } = host;
const wrapperClassName = `${baseClass}__table`;
const packItems = packs.map((pack) => {
if (pack_stats === null || pack_stats.length === 0) {
return <p>There are no packs for this host.</p>;
}
const packsAccordion = pack_stats.map((pack) => {
return (
<li className="list__item" key={pack.id}>
<Button
onClick={() => onPackClick(pack)}
variant="text-link"
className="list__button"
>
{pack.name}
</Button>
</li>
<AccordionItem key={pack.pack_id}>
<AccordionItemHeading>
<AccordionItemButton>{pack.pack_name}</AccordionItemButton>
</AccordionItemHeading>
<AccordionItemPanel>
{!pack.query_stats.length ? (
<div>There are no schedule queries for this pack.</div>
) : (
<div className={`${baseClass}__wrapper`}>
<table className={wrapperClassName}>
<thead>
<tr>
<th>Query Name</th>
<th>Description</th>
<th>Frequency</th>
<th>Last Run</th>
</tr>
</thead>
<tbody>
{!!pack.query_stats.length &&
pack.query_stats.map((query) => {
return (
<PackQueriesListRow
key={`pack-row-${query.pack_id}-${query.scheduled_query_id}`}
query={query}
/>
);
})}
</tbody>
</table>
</div>
)}
</AccordionItemPanel>
</AccordionItem>
);
});
@@ -258,12 +295,15 @@ export class HostDetailsPage extends Component {
{packs.length === 0 ? (
<p className="info__item">No packs have this host as a target.</p>
) : (
<ul className="list">{packItems}</ul>
<Accordion allowMultipleExpanded="true" allowZeroExpanded="true">
{packsAccordion}
</Accordion>
)}
</div>
);
};
// <ul className="list">{packItems}</ul>
renderSoftware = () => {
const { host } = this.props;
const wrapperClassName = `${baseClass}__table`;
@@ -0,0 +1,35 @@
import React, { Component } from "react";
import { humanQueryLastRun } from "kolide/helpers";
import queryInterface from "interfaces/query";
const baseClass = "pack-queries-list-row";
class PackQueriesListRow extends Component {
static propTypes = {
query: queryInterface.isRequired,
};
render() {
const { query } = this.props;
const {
scheduled_query_name,
description,
interval,
last_executed,
} = query;
return (
<tr>
<td className={`${baseClass}__name`}>{scheduled_query_name}</td>
<td className={`${baseClass}__description`}>{description}</td>
<td className={`${baseClass}__frequency`}>{interval} seconds</td>
<td className={`${baseClass}__last-run`}>
{humanQueryLastRun(last_executed)}
</td>
</tr>
);
}
}
export default PackQueriesListRow;
@@ -0,0 +1,29 @@
.pack-queries-list-row {
line-height: 38px;
border-bottom: 1px solid $ui-fleet-blue-15;
&__type {
max-width: 280px;
}
&__installed-version {
max-width: 280px;
}
&:last-child {
border-bottom: 0;
}
td {
font-size: $x-small;
.form-field {
margin: 0;
}
}
&__name {
@include ellipsis(120px);
display: table-cell;
}
}
@@ -0,0 +1 @@
export { default } from "./PackQueriesListRow";
@@ -251,7 +251,71 @@ a {
}
#back-chevron {
width: 12px;
margin-right: 10px;
width: 16px;
margin-right: $pad-small;
}
.accordion {
border-radius: 2px;
}
.accordion__item + .accordion__item {
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.accordion__button {
background-color: #fff;
color: $core-fleet-black;
cursor: pointer;
text-align: left;
font-size: $x-small;
font-weight: bold;
border: none;
padding: 17px 12px;
}
.accordion__button:hover {
background-color: $ui-vibrant-blue-10;
}
.accordion__button:after {
display: block;
content: url("../assets/images/icon-chevron-purple-9x6@2x.png");
text-align: center;
top: 50%;
float: right;
width: 32px;
height: 32px;
border-radius: 4px;
transform: scale(0.5) translate(-50%, -60%);
}
.accordion__button[aria-expanded="true"]::after,
.accordion__button[aria-selected="true"]::after {
background-color: $core-vibrant-blue;
content: url("../assets/images/icon-accordion-collapse-16x16@2x.png");
}
[hidden] {
display: none;
}
.accordion__panel {
padding: 20px;
animation: fadein 0.35s ease-in;
}
/* -------------------------------------------------- */
/* ---------------- Animation part ------------------ */
/* -------------------------------------------------- */
@keyframes fadein {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
}
+1
View File
@@ -39,6 +39,7 @@
"proxy-middleware": "0.15.0",
"rc-pagination": "1.16.3",
"react": "^16.8.6",
"react-accessible-accordion": "^3.3.4",
"react-ace": "^6.3.2",
"react-dom": "^16",
"react-entity-getter": "0.0.8",
+5
View File
@@ -10566,6 +10566,11 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-accessible-accordion@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/react-accessible-accordion/-/react-accessible-accordion-3.3.4.tgz#84b08015e74d6b3b225e0101b518b2ea8969e292"
integrity sha512-EUq+KmVRIIG5u1fR5XIbb2JU7w7NouLjuyfuPvnhuDIfNNWNYap1I8ijn2rdA6OQi1gGSRGbVzHs+3wxH/M0Sw==
react-ace@^6.3.2:
version "6.6.0"
resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-6.6.0.tgz#a79457ef03c3b1f8d4fc598a003b1d6ad464f1a0"