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.
    + ) : ( +
    + + + + + + + + + + + {!!pack.query_stats.length && + pack.query_stats.map((query) => { + return ( + + ); + })} + +
    Query NameDescriptionFrequencyLast Run
    +
    + )} +
    +
    ); }); @@ -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"