From 9d8e93ee1ccd1ed54e1566a3ab79ef6e55f12c3a Mon Sep 17 00:00:00 2001 From: noahtalerman <47070608+noahtalerman@users.noreply.github.com> Date: Mon, 25 Jan 2021 17:08:28 -0800 Subject: [PATCH] Reveal live query error information in Fleet UI (#224) This PR adds an "Errors" table to the live query UI. Summary of changes: - Errors table includes the columns `hostname`, `osquery_version`, and `error` - The errors table is only rendered when at least one host fails - Hosts with an osquery version less than 4.4.0 always display the error "upgrade osquery on this host to 4.4.0+ for error details" --- .../components/hosts/HostsTable/_styles.scss | 2 +- .../loaders/ProgressBar/_styles.scss | 4 +- .../QueryProgressDetails.jsx | 22 ++++-- .../queries/QueryProgressDetails/_styles.scss | 42 ++++++++++ .../QueryResultsTable/QueryResultsTable.jsx | 77 +++++++++++++------ .../QueryResultsTable.tests.jsx | 4 + .../queries/QueryResultsTable/_styles.scss | 65 +++++++++++++--- .../queries/QueryPage/QueryPage.tests.jsx | 10 ++- .../redux/nodes/entities/campaigns/helpers.js | 38 ++++++++- frontend/styles/var/colors.scss | 1 + frontend/test/stubs.js | 4 + 11 files changed, 222 insertions(+), 47 deletions(-) diff --git a/frontend/components/hosts/HostsTable/_styles.scss b/frontend/components/hosts/HostsTable/_styles.scss index d14b4b5399..69cad14aa4 100644 --- a/frontend/components/hosts/HostsTable/_styles.scss +++ b/frontend/components/hosts/HostsTable/_styles.scss @@ -63,7 +63,7 @@ &--offline { &:before { - background-color: $core-red; + background-color: $ui-dark-grey; border-radius: 100%; content: ' '; display: inline-block; diff --git a/frontend/components/loaders/ProgressBar/_styles.scss b/frontend/components/loaders/ProgressBar/_styles.scss index eac6050d31..fb28b40765 100644 --- a/frontend/components/loaders/ProgressBar/_styles.scss +++ b/frontend/components/loaders/ProgressBar/_styles.scss @@ -1,8 +1,10 @@ .progress-bar { display: flex; - background-color: $ui-medium-grey; + background-color: $ui-dark-grey; height: 10px; overflow: hidden; + border-radius: 2px; + margin-top: 4px; &__progress { height: 100%; diff --git a/frontend/components/queries/QueryProgressDetails/QueryProgressDetails.jsx b/frontend/components/queries/QueryProgressDetails/QueryProgressDetails.jsx index 7d0babf19a..86adaae10f 100644 --- a/frontend/components/queries/QueryProgressDetails/QueryProgressDetails.jsx +++ b/frontend/components/queries/QueryProgressDetails/QueryProgressDetails.jsx @@ -11,9 +11,20 @@ const baseClass = 'query-progress-details'; const QueryProgressDetails = ({ campaign, className, onRunQuery, onStopQuery, queryIsRunning, queryTimerMilliseconds, disableRun }) => { const { hosts_count: hostsCount } = campaign; + const { Metrics: metrics = {} } = campaign; + const { errors } = campaign; const totalHostsCount = get(campaign, ['totals', 'count'], 0); const totalRowsCount = get(campaign, ['query_results', 'length'], 0); + const onlineHostsTotalDisplay = metrics.OnlineHosts === 1 ? '1 host' : `${metrics.OnlineHosts} hosts`; + const onlineResultsTotalDisplay = totalRowsCount === 1 ? '1 result' : `${totalRowsCount} results`; + const offlineHostsTotalDisplay = metrics.OfflineHosts === 1 ? '1 host' : `${metrics.OfflineHosts} hosts`; + const failedHostsTotalDisplay = hostsCount.failed === 1 ? '1 host' : `${hostsCount.failed} hosts`; + let totalErrorsDisplay = '0 errors'; + if (errors) { + totalErrorsDisplay = errors.length === 1 ? '1 error' : `${errors.length} errors`; + } + const runQueryBtn = (
-
- {hasNoResults && No results found} + Results +
+ {hasNoResults && No results found. Check the table below for errors.} {!hasNoResults && renderTable()}
+ {hasErrors && +
+ Errors +
+ {renderErrorsTable()} +
+
+ }
); } diff --git a/frontend/components/queries/QueryResultsTable/QueryResultsTable.tests.jsx b/frontend/components/queries/QueryResultsTable/QueryResultsTable.tests.jsx index 86fcbee39e..a3c821cb4b 100644 --- a/frontend/components/queries/QueryResultsTable/QueryResultsTable.tests.jsx +++ b/frontend/components/queries/QueryResultsTable/QueryResultsTable.tests.jsx @@ -49,6 +49,10 @@ const campaignWithQueryResults = { { host_hostname: 'dfoihgsx', cwd: '/', directory: '/root' }, { host_hostname: 'abc123', cwd: '/', directory: '/root' }, ], + Metrics: { + OnlineHosts: 2, + OfflineHosts: 0, + }, hosts_count: { failed: 0, successful: 2, diff --git a/frontend/components/queries/QueryResultsTable/_styles.scss b/frontend/components/queries/QueryResultsTable/_styles.scss index d96f6e96fc..d000679130 100644 --- a/frontend/components/queries/QueryResultsTable/_styles.scss +++ b/frontend/components/queries/QueryResultsTable/_styles.scss @@ -2,11 +2,14 @@ display: flex; flex-direction: column; background-color: $white; - padding: $pad-base; width: 100%; min-height: calc(500px + (#{$pad-base} * 2)); box-sizing: border-box; - max-height: 75vh; + + &__table-title { + font-size: $x-small; + font-weight: $bold; + } &__button-wrap { @include clearfix; @@ -17,6 +20,8 @@ } &__filter-icon { + color: $ui-dark-grey; + &--is-active { color: $core-blue; } @@ -27,15 +32,14 @@ width: 378px; } - &__table-wrapper { + &__results-table-wrapper { display: flex; - flex-grow: 1; border: solid 1px $ui-borders; border-radius: 3px; - box-shadow: inset 0 0 8px 0 rgba(0, 0, 0, 0.12); overflow: scroll; - margin-top: 30px; - min-height: 400px; + margin-top: 4px; + min-height: 200px; + max-height: 400px; width: 100%; .kolide-spinner { @@ -46,13 +50,29 @@ flex-grow: 1; align-self: center; text-align: center; + font-size: $x-small; } } + &__error-table-container { + margin-top: 24px; + } + + &__error-table-wrapper { + display: flex; + border: solid 1px $ui-borders; + border-radius: 3px; + overflow: scroll; + margin-bottom: 40px; + margin-top: 4px; + max-height: 200px; + width: 100%; + } + &__table { border-collapse: collapse; color: $core-dark-blue-grey; - font-size: $small; + font-size: $x-small; width: 100%; } @@ -60,11 +80,16 @@ background-color: $core-light-blue-grey; color: $core-black; text-align: left; + border-bottom: 1px solid $ui-borders; th { - padding: $pad-small $pad-xsmall; + padding: 12px 24px; min-width: 125px; + .form-field { + margin-bottom: 0; + } + span { white-space: nowrap; @@ -83,12 +108,15 @@ background-color: $white; td { - padding: $pad-xsmall; + padding: 12px 24px; + white-space: nowrap; } tr { - &:nth-child(even) { - background-color: $core-light-blue-grey; + border-bottom: 1px solid $ui-borders; + + &:last-child { + border-bottom: 0; } } } @@ -100,10 +128,23 @@ box-shadow: 0 0 12px 0 rgba(0, 0, 0, 0.1); border: solid 1px $ui-borders; z-index: 99; + padding: 20px; .query-progress-details__run-btn { display: none; } + + .query-progress-details__stop-btn { + display: none; + } + + .query-results-table__results-table-wrapper { + height: auto; + } + + .query-results-table__error-table-container { + display: none; + } } &--shrinking { diff --git a/frontend/pages/queries/QueryPage/QueryPage.tests.jsx b/frontend/pages/queries/QueryPage/QueryPage.tests.jsx index 36005b0e56..0155eb0f1e 100644 --- a/frontend/pages/queries/QueryPage/QueryPage.tests.jsx +++ b/frontend/pages/queries/QueryPage/QueryPage.tests.jsx @@ -196,7 +196,7 @@ describe('QueryPage - component', () => { 'resets selected targets and removed the campaign when the hostname changes', () => { const queryResult = { org_name: 'Kolide', org_url: 'https://kolide.co' }; - const campaign = { id: 1, query_results: [queryResult], hosts_count: { total: 1 } }; + const campaign = { id: 1, query_results: [queryResult], hosts_count: { total: 1 }, Metrics: { OnlineHosts: 1, OfflineHosts: 0 } }; const props = { dispatch: noop, loadingQueries: false, @@ -232,6 +232,10 @@ describe('QueryPage - component', () => { successful: 1, total: 1, }, + Metrics: { + OnlineHosts: 1, + OfflineHosts: 0, + }, query_results: [queryResult], }; const queryResultsCSV = convertToCSV([queryResult]); @@ -263,6 +267,10 @@ describe('QueryPage - component', () => { successful: 1, total: 1, }, + Metrics: { + OnlineHosts: 1, + OfflineHosts: 0, + }, query_results: [queryResult], }; const Page = mount(); diff --git a/frontend/redux/nodes/entities/campaigns/helpers.js b/frontend/redux/nodes/entities/campaigns/helpers.js index 3fe3cf1e32..629110c029 100644 --- a/frontend/redux/nodes/entities/campaigns/helpers.js +++ b/frontend/redux/nodes/entities/campaigns/helpers.js @@ -10,14 +10,17 @@ const updateCampaignStateFromTotals = (campaign, { data }) => { const updateCampaignStateFromResults = (campaign, { data }) => { const queryResults = campaign.query_results || []; + const errors = campaign.errors || []; const hosts = campaign.hosts || []; - const { host, rows } = data; + const { host, rows, error } = data; const { hosts_count: hostsCount } = campaign; const newHosts = [...hosts, host]; const newQueryResults = [...queryResults, ...rows]; let newHostsCount; - - if (data.error) { + let newErrors; + // Host's with osquery version above 4.4.0 receive an error message + // when the live query fails. + if (error) { const newFailed = hostsCount.failed + 1; const newTotal = hostsCount.successful + newFailed; @@ -26,6 +29,34 @@ const updateCampaignStateFromResults = (campaign, { data }) => { failed: newFailed, total: newTotal, }; + + newErrors = [ + ...errors, + { + host_hostname: host.hostname, + osquery_version: host.osquery_version, + error, + }, + ]; + // Host's with osquery version below 4.4.0 receive an empty error message + // when the live query fails so we create our own message. + } else if (error === '') { + const newFailed = hostsCount.failed + 1; + const newTotal = hostsCount.successful + newFailed; + + newHostsCount = { + successful: hostsCount.successful, + failed: newFailed, + total: newTotal, + }; + newErrors = [ + ...errors, + { + host_hostname: host.hostname, + osquery_version: host.osquery_version, + error: 'upgrade osquery on this host to 4.4.0+ for error details', + }, + ]; } else { const newSuccessful = hostsCount.successful + 1; const newTotal = hostsCount.failed + newSuccessful; @@ -43,6 +74,7 @@ const updateCampaignStateFromResults = (campaign, { data }) => { hosts: newHosts, query_results: newQueryResults, hosts_count: newHostsCount, + errors: newErrors, }, }; }; diff --git a/frontend/styles/var/colors.scss b/frontend/styles/var/colors.scss index a8a6d0a4d8..ab2b7276a3 100644 --- a/frontend/styles/var/colors.scss +++ b/frontend/styles/var/colors.scss @@ -16,6 +16,7 @@ $core-blue-green: #25c3ba; $ui-borders: #dbe3e5; $ui-light-grey: #fafafa; $ui-medium-grey: #e3e3e3; +$ui-dark-grey: #afbec1; $warning: #ffad00; // Colors for over (hover) and down (active) buttons styles diff --git a/frontend/test/stubs.js b/frontend/test/stubs.js index cd3cd300be..2411e13eb2 100644 --- a/frontend/test/stubs.js +++ b/frontend/test/stubs.js @@ -175,6 +175,10 @@ export const campaignStub = { successful: 2, total: 2, }, + Metrics: { + OnlineHosts: 2, + OfflineHosts: 0, + }, id: 1, query_id: queryStub.id, query_results: [queryResultStub],