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:
co-authored by
Sarah Gillespie
parent
02973ec4a2
commit
d4147e916d
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 |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user