diff --git a/.sass-lint.yml b/.sass-lint.yml index 967dabe2ee..9e991d882c 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -69,7 +69,7 @@ rules: no-css-comments: 1 no-duplicate-properties: 1 no-empty-rulesets: 1 - no-extends: 1 + no-extends: 0 no-ids: 1 no-important: 1 no-invalid-hex: 1 diff --git a/assets/fonts/kolidecons/kolidecons.woff b/assets/fonts/kolidecons/kolidecons.woff index 0cd13cc0b0..62a532bc51 100644 Binary files a/assets/fonts/kolidecons/kolidecons.woff and b/assets/fonts/kolidecons/kolidecons.woff differ diff --git a/frontend/components/forms/fields/InputField/_styles.scss b/frontend/components/forms/fields/InputField/_styles.scss index b72379e5b9..f736dc7df0 100644 --- a/frontend/components/forms/fields/InputField/_styles.scss +++ b/frontend/components/forms/fields/InputField/_styles.scss @@ -4,7 +4,7 @@ border-radius: 2px; background-color: $white; border: solid 1px $accent-medium; - font-size: $base; + font-size: 16px; padding: $pad-xsmall $pad-base; color: $text-dark; font-family: 'Oxygen', sans-serif; diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.jsx b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.jsx index 5702f003f7..16c255b289 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.jsx +++ b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsDropdown.jsx @@ -1,5 +1,6 @@ import React, { Component, PropTypes } from 'react'; import { isEqual, noop } from 'lodash'; +import classnames from 'classnames'; import Kolide from 'kolide'; import targetInterface from 'interfaces/target'; @@ -22,6 +23,7 @@ class SelectTargetsDropdown extends Component { super(props); this.state = { + isEmpty: false, isLoadingTargets: false, moreInfoTarget: null, query: '', @@ -57,8 +59,6 @@ class SelectTargetsDropdown extends Component { const currentMoreInfoTarget = this.state.moreInfoTarget || {}; if (isEqual(moreInfoTarget.display_text, currentMoreInfoTarget.display_text)) { - this.setState({ moreInfoTarget: null }); - return false; } @@ -75,13 +75,18 @@ class SelectTargetsDropdown extends Component { }); } - this.setState({ moreInfoTarget }); return false; }; } + onBackToResults = () => { + this.setState({ + moreInfoTarget: null, + }); + } + fetchTargets = (query, selectedTargets = this.props.selectedTargets) => { const { onFetchTargets } = this.props; @@ -93,6 +98,14 @@ class SelectTargetsDropdown extends Component { targets, } = response; + if (targets.length === 0) { + // We don't want the lib's default "No Results" so we fake it + targets.push({}); + this.setState({ isEmpty: true }); + } else { + this.setState({ isEmpty: false }); + } + onFetchTargets(query, response); this.setState({ isLoadingTargets: false, targets }); @@ -107,13 +120,19 @@ class SelectTargetsDropdown extends Component { } render () { - const { isLoadingTargets, moreInfoTarget, targets } = this.state; - const { fetchTargets, onInputClose, onTargetSelectMoreInfo } = this; + const { isEmpty, isLoadingTargets, moreInfoTarget, targets } = this.state; + const { fetchTargets, onBackToResults, onInputClose, onTargetSelectMoreInfo } = this; const { onSelect, selectedTargets } = this.props; - const menuRenderer = Menu(onTargetSelectMoreInfo, moreInfoTarget); + const menuRenderer = Menu(onTargetSelectMoreInfo, moreInfoTarget, onBackToResults); + + const inputClasses = classnames({ + 'show-preview': moreInfoTarget, + 'is-empty': isEmpty, + }); return ( diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/SelectTargetsMenu.jsx b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/SelectTargetsMenu.jsx index 84a2ea4882..952ac09efb 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/SelectTargetsMenu.jsx +++ b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/SelectTargetsMenu.jsx @@ -6,8 +6,9 @@ import targetInterface from 'interfaces/target'; import TargetDetails from '../TargetDetails'; import TargetOption from '../TargetOption'; +const baseClass = 'target-list'; -const SelectTargetsMenuWrapper = (onMoreInfoClick, moreInfoTarget) => { +const SelectTargetsMenuWrapper = (onMoreInfoClick, moreInfoTarget, handleBackToResults) => { const SelectTargetsMenu = ({ focusedOption, instancePrefix, @@ -21,9 +22,14 @@ const SelectTargetsMenuWrapper = (onMoreInfoClick, moreInfoTarget) => { onOptionRef, }) => { const Option = optionComponent; + const renderTargets = (targetType) => { const targets = filter(options, { target_type: targetType }); + if (targets.length === 0) { + return Unable to find any matching {targetType}.; + } + return targets.map((target, index) => { const { disabled: isDisabled } = target; const isSelected = includes(valueArray, target); @@ -61,14 +67,14 @@ const SelectTargetsMenuWrapper = (onMoreInfoClick, moreInfoTarget) => { }; return ( -
-
-
hosts
- {renderTargets('hosts')} -
labels
+
+
+

labels

{renderTargets('labels')} +

hosts

+ {renderTargets('hosts')}
- +
); }; diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/_styles.scss b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/_styles.scss new file mode 100644 index 0000000000..2ac4dbed79 --- /dev/null +++ b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/_styles.scss @@ -0,0 +1,45 @@ +.target-list { + &__type { + background-color: $bg-medium; + line-height: $larger; + padding: 0 $base; + margin: 0; + text-transform: uppercase; + font-weight: $light; + font-size: $mini; + color: $text-dark; + letter-spacing: 0.5px; + } + + &__options { + width: 57%; + float: left; + overflow-y: auto; + box-shadow: 0 8px 12px 0 rgba(0, 0, 0, 0.15); + min-height: 500px; + position: relative; + z-index: 2; + background-color: $bg-light; + } + + &__spotlight { + width: 43%; + float: right; + overflow-y: auto; + background-color: $white; + min-height: 500px; + position: relative; + z-index: 1; + } + + &__not-found { + font-size: $small; + font-weight: $bold; + line-height: 2.64; + text-align: left; + color: $text-dark; + text-transform: uppercase; + display: block; + padding: 0 $large; + } +} diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/TargetDetails.jsx b/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/TargetDetails.jsx index d705a11fc0..46fc4dbc4a 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/TargetDetails.jsx +++ b/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/TargetDetails.jsx @@ -1,10 +1,9 @@ -import React, { Component } from 'react'; +import React, { Component, PropTypes } from 'react'; +import { noop } from 'lodash'; import AceEditor from 'react-ace'; import classnames from 'classnames'; import hostHelpers from 'components/hosts/HostDetails/helpers'; -import ShadowBox from 'components/ShadowBox'; -import ShadowBoxInput from 'components/forms/fields/ShadowBoxInput'; import targetInterface from 'interfaces/target'; const baseClass = 'target-details'; @@ -12,10 +11,28 @@ const baseClass = 'target-details'; class TargetDetails extends Component { static propTypes = { target: targetInterface, + className: PropTypes.string, + handleBackToResults: PropTypes.func, + }; + + static defaultProps = { + handleBackToResults: noop, + }; + + onlineHosts = (labelBaseClass, online) => { + if (online > 0) { + return ( + ({online}% ONLINE) + ); + } + + return false; }; renderHost = () => { + const { className, handleBackToResults, target } = this.props; const { + display_text: displayText, ip, mac, memory, @@ -23,7 +40,7 @@ class TargetDetails extends Component { osVersion, platform, status, - } = this.props.target; + } = target; const hostBaseClass = 'host-target'; const isOnline = status === 'online'; const isOffline = status === 'offline'; @@ -34,63 +51,104 @@ class TargetDetails extends Component { ); return ( -
-

{status}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
IP Address{ip}
MAC Address{mac}
Platform - - {platform} -
Operating System{osVersion}
Osquery Version{osqueryVersion}
Memory{hostHelpers.humanMemory(memory)}
-
+
+ + +

+ + {displayText} +

+

+ {isOnline && } + {isOffline && } + {status} +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
IP Address{ip}
MAC Address{mac}
Platform + + {platform} +
Operating System{osVersion}
Osquery Version{osqueryVersion}
Memory{hostHelpers.humanMemory(memory)}
-
- +

+ Labels -

+

+
    +
  • Engineering
  • +
  • DevOps
  • +
  • ElCapDev
  • +
  • Workstation
  • +
); } renderLabel = () => { - const { hosts, query } = this.props.target; + const { onlineHosts } = this; + const { handleBackToResults, className, target } = this.props; + const { + count, + description, + display_text: displayText, + online, + query, + } = target; const labelBaseClass = 'label-target'; return ( -
-
+
+ + +

+ + {displayText} +

+ +

+ {count}HOSTS + { onlineHosts(labelBaseClass, online) } +

+ +

{description || 'No Description'}

+ +
-
- -
- {hosts.length} HOSTS -
-
- - - - - - - - - - - - - {hosts.map((host) => { - return ( - - - - - - - - ); - })} - -
HostnameStatusPlatformLocationMAC
{host.hostname}{host.status}{host.ip}{host.mac}
-
); } diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/_styles.scss b/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/_styles.scss new file mode 100644 index 0000000000..109a0be625 --- /dev/null +++ b/frontend/components/forms/fields/SelectTargetsDropdown/TargetDetails/_styles.scss @@ -0,0 +1,181 @@ +.host-target, +.label-target { + padding: $pad-large; + + &__back { + color: $text-dark; + font-size: $xsmall; + display: none; + + @include breakpoint(tablet) { + display: inline-block; + position: absolute; + top: 10px; + left: 10px; + padding: 10px; + + &:active { + top: 10px; + } + } + } + + @include breakpoint(tablet) { + padding-top: $pad-most; + } +} + +.host-target { + &__display-text { + font-size: $medium; + font-weight: $bold; + letter-spacing: 0.6px; + color: $text-ultradark; + margin: 0; + } + + &__icon { + color: $accent-text; + margin-right: $pad-small; + width: 21px; + + &--online { + color: $success; + } + + &--offline { + color: $alert; + } + } + + &__status { + margin: $pad-xsmall 0; + font-size: $small; + font-weight: $bold; + letter-spacing: 1.3px; + text-transform: uppercase; + + &--is-online { + color: $success; + } + + &--is-offline { + color: $alert; + } + } + + &__labels-wrapper { + position: absolute; + bottom: $pad-large; + } + + &__labels-header { + font-size: $small; + font-weight: $bold; + letter-spacing: 0.9px; + color: $text-dark; + text-transform: uppercase; + margin: 0; + } + + &__labels-list { + font-size: $small; + font-weight: $normal; + line-height: 1.85; + letter-spacing: 0.5px; + color: $text-medium; + list-style: none; + margin: 0; + padding: 0; + + li { + display: inline-block; + + &::after { + content: ','; + margin-right: 3px; + } + + &:last-child { + &::after { + content: ''; + margin: 0; + } + } + } + } + + &__platform-text { + text-transform: capitalize; + } + + &__mac-address { + text-transform: uppercase; + } +} + +.label-target { + &__display-text { + font-size: $medium; + font-weight: $bold; + letter-spacing: 0.6px; + color: $text-ultradark; + margin: 0; + } + + &__icon { + color: $accent-text; + margin-right: $pad-small; + width: 21px; + } + + &__hosts { + margin: $pad-xsmall 0; + letter-spacing: 0.4px; + font-size: $small; + font-weight: $normal; + } + + &__hosts-count { + font-weight: $bold; + color: $text-dark; + + strong { + min-width: 21px; + text-align: center; + font-weight: $bold; + display: inline-block; + margin-right: $pad-small; + } + } + + &__hosts-online { + color: $success; + } + + &__description { + font-size: $pad-medium; + font-weight: $normal; + line-height: 1.71; + color: $text-dark; + margin: 0 0 $pad-small; + } +} + +.target-details { + &__table { + border-collapse: collapse; + border-spacing: 0; + text-align: left; + font-size: $small; + width: 100%; + margin-bottom: $pad-medium; + + th { + font-weight: $bold; + line-height: 2.3; + color: $text-dark; + padding-right: $pad-half; + } + } +} diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/TargetOption/TargetOption.jsx b/frontend/components/forms/fields/SelectTargetsDropdown/TargetOption/TargetOption.jsx index c5da62e29d..f6d1dd5005 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/TargetOption/TargetOption.jsx +++ b/frontend/components/forms/fields/SelectTargetsDropdown/TargetOption/TargetOption.jsx @@ -29,7 +29,12 @@ class TargetOption extends Component { const { count, ip, target_type: targetType } = target; if (targetType === 'hosts') { - return {ip}; + return ( + + + {ip} + + ); } return {count} hosts; @@ -57,7 +62,6 @@ class TargetOption extends Component { {targetType === 'hosts' && } {targetType === 'labels' && } {displayText} - {renderTargetDetail()}
diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/TargetOption/_styles.scss b/frontend/components/forms/fields/SelectTargetsDropdown/TargetOption/_styles.scss index 8f03982987..506b90a037 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/TargetOption/_styles.scss +++ b/frontend/components/forms/fields/SelectTargetsDropdown/TargetOption/_styles.scss @@ -7,13 +7,41 @@ font-size: 15px; } + &__add-btn { + color: $accent-medium; + font-size: $medium; + padding: 0 $pad-half; + } + + &__wrapper { + @include display(flex); + @include align-items(stretch); + @include align-content(stretch); + cursor: default; + } + + &__target-content, + &__add-btn { + line-height: 34px; + font-size: $small; + font-weight: $normal; + text-transform: none; + } + + &__target-content { + @include flex-grow(1); + text-align: left; + padding-right: $pad-half; + } + &__count { background-color: $text-medium; border-radius: $border-radius; color: $white; - font-size: $small; + font-size: 13px; font-weight: bold; padding: 5px; + margin-left: $pad-half; } &__delimeter { @@ -24,25 +52,24 @@ &__icon { color: $text-medium; - font-size: $base; - margin: 0 0 0 $pad-base; + font-size: $small; } &__ip { - color: $link; + color: $text-dark; font-size: $small; } &__label-host { color: $link; font-size: $base; - margin-left: $pad-xsmall; + margin-left: $pad-half; } &__label-label { color: $text-medium; - font-size: $base; - margin-left: $pad-xsmall; + font-size: 14px; + margin-left: $pad-half; .all-hosts { font-weight: $bold; @@ -65,8 +92,4 @@ &__target-modal { width: 1000px; } - - &__wrapper { - cursor: default; - } } diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss b/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss index 0e5713dbd5..89b8bc5199 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss +++ b/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss @@ -1,26 +1,45 @@ .target-select { &.Select { .Select-control { + @include transition(border 150ms ease-in-out, box-shadow 150ms ease-in-out); height: 48px; } + .Select-arrow { + border-width: 8px 8px 2.5px; + } + .Select-input { height: 46px; + margin: 0 0 0 $pad-base; > input { line-height: 30px; padding: 8px 0; + color: $text-ultradark; + font-size: 16px; } } .Select-placeholder { line-height: 46px; + font-size: 16px; + padding: 0 $pad-base; + } + + .Select-arrow-zone { + padding-right: 19px; } &.Select--multi { .Select-option { + padding: 0; + background-color: $bg-light; + &.is-disabled { &.is-focused { + background-color: $brand-light; + .target-option__label-label, .target-option__target-icon, .target-option__delimeter, @@ -29,13 +48,163 @@ .target-option__icon { color: $white; } + + .target-option__add-btn { + color: $brand-ultralight; + } + + .target-option__count { + background-color: $white; + color: $brand; + } } } + + &:last-child { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } } .Select-value { line-height: 1.9; } } + + &.is-focused { + .Select-control { + border: 1px solid $accent-medium; + border-bottom-color: $brand; + box-shadow: inset 0 0 8px 0 rgba($black, 0.1); + } + } + } + + .Select-menu-outer { + max-height: 500px; + height: 500px; + overflow: hidden; + box-shadow: 0 8px 12px 0 rgba(0, 0, 0, 0.15); + + .Select-menu { + height: 498px; + max-height: 498px; + overflow: hidden; + position: relative; + + &::after { + @extend %kolidecon; + content: '\f03c'; + font-size: 150px; + position: absolute; + left: 57%; + top: 50%; + margin-top: -75px; + opacity: 0.25; + width: 43%; + text-align: center; + } + } + + .target-list { + height: 100%; + } + } + + .Select-value { + border-radius: 2px; + background-color: $white; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12), 0 2px 0 0 $accent-dark; + border: solid 1px $accent-dark; + + .Select-value-icon { + border: 0; + float: right; + visibility: hidden; + position: relative; + padding: 0 $pad-medium; + line-height: 34px; + + &::after { + @extend %kolidecon; + @include transition(color 150ms ease-in-out); + @include transform(translate(-50%, -50%)); + content: '\f036'; + position: absolute; + top: 50%; + left: 50%; + visibility: visible; + font-size: 20px; + color: $accent-dark; + } + + &:hover { + &::after { + color: $accent-text; + } + } + } + + .Select-value-label { + font-size: 16px; + font-weight: $normal; + color: $text-dark; + padding: 0 0 0 $pad-medium; + line-height: 34px; + } + } + + .Select-clear { + font-size: 28px; + margin-right: 10px; + color: $text-dark; + + &:hover { + color: $alert; + } + } + + .Select-loading-zone { + padding-right: 10px; + } + + &.is-empty { + .Select-menu-outer { + .Select-menu { + &::after { + content: '\f049'; + font-size: 180px; + margin-top: -90px; + color: $accent-medium; + } + } + } + } + + @include breakpoint(tablet) { + .Select-menu-outer { + .Select-menu { + min-width: 665px; + + &::after { + display: none; + } + } + } + + &.show-preview { + .Select-menu-outer { + .Select-menu { + left: auto; + right: 0; + } + } + + .Select-menu { + left: 0; + right: auto; + position: absolute; + } + } } } diff --git a/frontend/components/queries/QueryComposer/QueryComposer.jsx b/frontend/components/queries/QueryComposer/QueryComposer.jsx index f0f1326aa2..7052a95465 100644 --- a/frontend/components/queries/QueryComposer/QueryComposer.jsx +++ b/frontend/components/queries/QueryComposer/QueryComposer.jsx @@ -91,9 +91,9 @@ class QueryComposer extends Component { return (
-

+

Select Targets - {selectedTargetsCount} unique hosts + {selectedTargetsCount} unique {selectedTargetsCount === 1 ? 'host' : 'hosts' }

+

New Query