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)} |
+
+
+
-
+
+
+ - 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
-
-
-
-
-
-
- | Hostname |
- Status |
- Platform |
- Location |
- MAC |
-
-
-
- {hosts.map((host) => {
- return (
-
- | {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