diff --git a/assets/images/icon-accordion-collapse-16x16@2x.png b/assets/images/icon-accordion-collapse-16x16@2x.png
new file mode 100644
index 0000000000..1234f114dd
Binary files /dev/null and b/assets/images/icon-accordion-collapse-16x16@2x.png differ
diff --git a/assets/images/icon-chevron-purple-9x6@2x.png b/assets/images/icon-chevron-purple-9x6@2x.png
new file mode 100644
index 0000000000..32f1662b7e
Binary files /dev/null and b/assets/images/icon-chevron-purple-9x6@2x.png differ
diff --git a/assets/images/icon-chevron-white-9x6@2x.png b/assets/images/icon-chevron-white-9x6@2x.png
new file mode 100644
index 0000000000..71f774b722
Binary files /dev/null and b/assets/images/icon-chevron-white-9x6@2x.png differ
diff --git a/frontend/interfaces/query.ts b/frontend/interfaces/query.ts
index b4776cfd62..2ac7806c29 100644
--- a/frontend/interfaces/query.ts
+++ b/frontend/interfaces/query.ts
@@ -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;
}
diff --git a/frontend/kolide/helpers.ts b/frontend/kolide/helpers.ts
index 64d1e22b2b..70b13c5069 100644
--- a/frontend/kolide/helpers.ts
+++ b/frontend/kolide/helpers.ts
@@ -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,
};
diff --git a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.jsx b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.jsx
index 98a989fcf1..0f6c403589 100644
--- a/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.jsx
+++ b/frontend/pages/hosts/HostDetailsPage/HostDetailsPage.jsx
@@ -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
There are no packs for this host.
;
+ }
+
+ const packsAccordion = pack_stats.map((pack) => {
return (
-
-
-
+
+
+ {pack.pack_name}
+
+
+ {!pack.query_stats.length ? (
+ There are no schedule queries for this pack.
+ ) : (
+
+
+
+
+ | Query Name |
+ Description |
+ Frequency |
+ Last Run |
+
+
+
+ {!!pack.query_stats.length &&
+ pack.query_stats.map((query) => {
+ return (
+
+ );
+ })}
+
+
+
+ )}
+
+
);
});
@@ -258,12 +295,15 @@ export class HostDetailsPage extends Component {
{packs.length === 0 ? (
No packs have this host as a target.
) : (
-
+
+ {packsAccordion}
+
)}
);
};
+ //
renderSoftware = () => {
const { host } = this.props;
const wrapperClassName = `${baseClass}__table`;
diff --git a/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/PackQueriesListRow.jsx b/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/PackQueriesListRow.jsx
new file mode 100644
index 0000000000..0899f7faa7
--- /dev/null
+++ b/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/PackQueriesListRow.jsx
@@ -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 (
+
+ | {scheduled_query_name} |
+ {description} |
+ {interval} seconds |
+
+ {humanQueryLastRun(last_executed)}
+ |
+
+ );
+ }
+}
+
+export default PackQueriesListRow;
diff --git a/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/_styles.scss b/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/_styles.scss
new file mode 100644
index 0000000000..a6665e0828
--- /dev/null
+++ b/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/_styles.scss
@@ -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;
+ }
+}
diff --git a/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/index.js b/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/index.js
new file mode 100644
index 0000000000..926ed24fa8
--- /dev/null
+++ b/frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/index.js
@@ -0,0 +1 @@
+export { default } from "./PackQueriesListRow";
diff --git a/frontend/pages/hosts/HostDetailsPage/_styles.scss b/frontend/pages/hosts/HostDetailsPage/_styles.scss
index f56ed3adba..7710ccd051 100644
--- a/frontend/pages/hosts/HostDetailsPage/_styles.scss
+++ b/frontend/pages/hosts/HostDetailsPage/_styles.scss
@@ -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;
+ }
}
}
diff --git a/package.json b/package.json
index 17dad6ca43..019240a97a 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/yarn.lock b/yarn.lock
index 38012508fd..da028184a9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"