From d4147e916dce496dcd3e714348f9618bbb8a7742 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 7 May 2021 18:18:52 -0400 Subject: [PATCH] Host details page: packs accordion (#724) * Pack queries accordion * Connects to query_stats API * No packs edge case Co-authored-by: Sarah Gillespie --- .../icon-accordion-collapse-16x16@2x.png | Bin 0 -> 565 bytes assets/images/icon-chevron-purple-9x6@2x.png | Bin 0 -> 982 bytes assets/images/icon-chevron-white-9x6@2x.png | Bin 0 -> 224 bytes frontend/interfaces/query.ts | 4 ++ frontend/kolide/helpers.ts | 11 +++ .../hosts/HostDetailsPage/HostDetailsPage.jsx | 66 +++++++++++++---- .../PackQueriesListRow/PackQueriesListRow.jsx | 35 +++++++++ .../PackQueriesListRow/_styles.scss | 29 ++++++++ .../PackQueriesListRow/index.js | 1 + .../pages/hosts/HostDetailsPage/_styles.scss | 68 +++++++++++++++++- package.json | 1 + yarn.lock | 5 ++ 12 files changed, 205 insertions(+), 15 deletions(-) create mode 100644 assets/images/icon-accordion-collapse-16x16@2x.png create mode 100644 assets/images/icon-chevron-purple-9x6@2x.png create mode 100644 assets/images/icon-chevron-white-9x6@2x.png create mode 100644 frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/PackQueriesListRow.jsx create mode 100644 frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/_styles.scss create mode 100644 frontend/pages/hosts/HostDetailsPage/PackQueriesListRow/index.js 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 0000000000000000000000000000000000000000..1234f114dd7db53cc8ada783220076e8d1d456f7 GIT binary patch literal 565 zcmV-50?Pe~P)L5>`n~r@08F&CD5D7ir zQ$(R4V8PzSFCnG3h5zg6X_`aukU#D0g+fay2GRgWu>c5U>;JPIdMi~bk=pRFPYMNI z>$Gsr-~#>-X0ldRQL-#*d($&YBry(*g00000NkvXXu0mjf D-E-`O literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..32f1662b7ec160934ef2582a529f7606a905894c GIT binary patch literal 982 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg(UKbBnda-upao=eFt9QT zF)#yJj6lf1D8&FW4aj2fVw8rngBUfSYM2-p+A|qgplYIkGzd%pVvrsP&AfmSVd4TN zxN3z3%m_9}+rE9xX+Vmzz$3Dlfk8|agc&`9R6YPri_8p(D2ed(u}aR*)k{ptPfFFR z$SnZrVz8;O0y1+`OA-|-a&z*EttxDlz$&bOY>=?Nk^)#sNw%$0gl~X?bAC~(f~lT~ zo`I4bmx6+VO;JjkRgjAtR6CGotCUevQedU8UtV6WS8lAAUzDzIXlZGwZ(yWvWTXpJ zp<7&;SCUwvn^&w1F$89gOKNd)QD#9&W`3Rm$lS!F{L&IzB_)tWZ~$>KW+6%?4_<0f}1q7iFdbZ3dZdXJ`Xfi6REI$3`DyIg(=_J_U;cy=up0 zqYn=@J1(C?>XE=e4)=6%42j@;8{`{y$UtD8XZN1xUCI?K>mTrbVcEtl^2*I$VV=Vc zkrvhprag_}A2i<7tWP@Tvti3NPtVfKw2MCbe*XUD%dn@Cbq`Zq(eF|YF4N<8PA{9Ew$W>Vpov{i*l8|3*eT=oq(*6#7*;iNJbPBSy1Lq}G1 zt+bEV&M}|UtTOFng7n7hP4b5=85dr6TsrUI91ey@tHQL8Dr*NHl~ne8ti{))X6nP} zr~Tkiio^{+q)uJM)S+cw;9WN@a_!kxJJNw_1 zF)u^uVw%PGAHs}z274xDyI7>p=ZITYTzGKb>eh9g+xEOOJa*H1uE2q1vlDYpf8G&( hK>LB{2d*0;KbRafyW`FnF5C@Dt)8xaF6*2UngEntH*)|0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..71f774b7229e60ed47b14565123982a88641d0c1 GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^LO{&J!3HGrh2HJ~Qk(@Ik;M!QVyYm_=ozH)0Vv2= z9OUlAu~-Ub667HvjNk6DvF9S*7Z zFbPdC;#@jG&_lC!+nY)L)7I=YQ)M#vtP-hu`{S~>C6=Fkm&q+L{5adh>5|&yL#>9K zo{KJqWSMLXHM*WKQ@!%ak4^SMD!c90Y98E~{6pxaW+Q`F*qW&eT~3}8eq)og^ytL= Ss~JECFnGH9xvX { 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.

    ) : ( -
      {packItems}
    + + {packsAccordion} + )} ); }; + //
      {packItems}
    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"