diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9509b8ef..55256d8179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ ### Features 1. [#1477](https://github.com/influxdata/chronograf/pull/1477): Add ability to log alerts - 1. [#1474](https://github.com/influxdata/chronograf/pull/1474): Change behavior of template variable autocomplete to filter by exact match from front of string 1. [#1491](https://github.com/influxdata/chronograf/pull/1491): Update go vendoring to dep and committed vendor directory + 1. [#1500](https://github.com/influxdata/chronograf/pull/1500): Add autocomplete functionality to Template Variable dropdowns 1. [#1498](https://github.com/influxdata/chronograf/pull/1498): Notify user via UI when local settings are cleared ### UI Improvements diff --git a/ui/src/alerts/components/AlertsTable.js b/ui/src/alerts/components/AlertsTable.js index 763e8af85c..1e20964152 100644 --- a/ui/src/alerts/components/AlertsTable.js +++ b/ui/src/alerts/components/AlertsTable.js @@ -34,15 +34,16 @@ const AlertsTable = React.createClass({ filterAlerts(searchTerm, newAlerts) { const alerts = newAlerts || this.props.alerts + const filterText = searchTerm.toLowerCase() const filteredAlerts = alerts.filter(h => { if (h.host === null || h.name === null || h.level === null) { return false } return ( - h.name.toLowerCase().search(searchTerm.toLowerCase()) !== -1 || - h.host.toLowerCase().search(searchTerm.toLowerCase()) !== -1 || - h.level.toLowerCase().search(searchTerm.toLowerCase()) !== -1 + h.name.toLowerCase().includes(filterText) || + h.host.toLowerCase().includes(filterText) || + h.level.toLowerCase().includes(filterText) ) }) this.setState({searchTerm, filteredAlerts}) diff --git a/ui/src/dashboards/components/TemplateControlBar.js b/ui/src/dashboards/components/TemplateControlBar.js index 073f15e70e..3ef54e84c3 100644 --- a/ui/src/dashboards/components/TemplateControlBar.js +++ b/ui/src/dashboards/components/TemplateControlBar.js @@ -24,6 +24,7 @@ const TemplateControlBar = ({ onSelectTemplate(id, [item].map(x => omit(x, 'text')))} diff --git a/ui/src/data_explorer/components/MeasurementList.js b/ui/src/data_explorer/components/MeasurementList.js index f4b7f68c83..8bf56c9de1 100644 --- a/ui/src/data_explorer/components/MeasurementList.js +++ b/ui/src/data_explorer/components/MeasurementList.js @@ -115,8 +115,9 @@ const MeasurementList = React.createClass({ ) } + const filterText = this.state.filterText.toLowerCase() const measurements = this.state.measurements.filter(m => - m.match(this.state.filterText) + m.toLowerCase().includes(filterText) ) return ( diff --git a/ui/src/data_explorer/components/QueryEditor.js b/ui/src/data_explorer/components/QueryEditor.js index 816376f1a8..bfd038e428 100644 --- a/ui/src/data_explorer/components/QueryEditor.js +++ b/ui/src/data_explorer/components/QueryEditor.js @@ -162,9 +162,12 @@ class QueryEditor extends Component { if (matched && !_.isEmpty(templates)) { // maintain cursor poition const start = this.editor.selectionStart + const end = this.editor.selectionEnd + const filterText = matched[0].substr(1).toLowerCase() + const filteredTemplates = templates.filter(t => - t.tempVar.startsWith(matched[0]) + t.tempVar.toLowerCase().includes(filterText) ) const found = filteredTemplates.find( diff --git a/ui/src/data_explorer/components/TagListItem.js b/ui/src/data_explorer/components/TagListItem.js index f1d54c1d33..c3653e1225 100644 --- a/ui/src/data_explorer/components/TagListItem.js +++ b/ui/src/data_explorer/components/TagListItem.js @@ -52,7 +52,8 @@ const TagListItem = React.createClass({ return
no tag values
} - const filtered = tagValues.filter(v => v.match(this.state.filterText)) + const filterText = this.state.filterText.toLowerCase() + const filtered = tagValues.filter(v => v.toLowerCase().includes(filterText)) return (
diff --git a/ui/src/hosts/components/HostsTable.js b/ui/src/hosts/components/HostsTable.js index 8a7ab80f68..0694821989 100644 --- a/ui/src/hosts/components/HostsTable.js +++ b/ui/src/hosts/components/HostsTable.js @@ -35,20 +35,21 @@ const HostsTable = React.createClass({ }, filter(allHosts, searchTerm) { + const filterText = searchTerm.toLowerCase() return allHosts.filter(h => { const apps = h.apps ? h.apps.join(', ') : '' // search each tag for the presence of the search term let tagResult = false if (h.tags) { tagResult = Object.keys(h.tags).reduce((acc, key) => { - return acc || h.tags[key].search(searchTerm) !== -1 + return acc || h.tags[key].toLowerCase().includes(filterText) }, false) } else { tagResult = false } return ( - h.name.search(searchTerm) !== -1 || - apps.search(searchTerm) !== -1 || + h.name.toLowerCase().includes(filterText) || + apps.toLowerCase().includes(filterText) || tagResult ) }) diff --git a/ui/src/shared/components/Dropdown.js b/ui/src/shared/components/Dropdown.js index 52867449e8..091363af82 100644 --- a/ui/src/shared/components/Dropdown.js +++ b/ui/src/shared/components/Dropdown.js @@ -3,13 +3,19 @@ import {Link} from 'react-router' import classnames from 'classnames' import OnClickOutside from 'shared/components/OnClickOutside' import FancyScrollbar from 'shared/components/FancyScrollbar' -import {DROPDOWN_MENU_MAX_HEIGHT, DROPDOWN_MENU_ITEM_THRESHOLD} from 'shared/constants/index' +import { + DROPDOWN_MENU_MAX_HEIGHT, + DROPDOWN_MENU_ITEM_THRESHOLD, +} from 'shared/constants/index' class Dropdown extends Component { constructor(props) { super(props) this.state = { isOpen: false, + searchTerm: '', + filteredItems: this.props.items, + highlightedItemIndex: null, } this.handleClickOutside = ::this.handleClickOutside @@ -17,6 +23,10 @@ class Dropdown extends Component { this.handleSelection = ::this.handleSelection this.toggleMenu = ::this.toggleMenu this.handleAction = ::this.handleAction + this.handleFilterChange = ::this.handleFilterChange + this.applyFilter = ::this.applyFilter + this.handleFilterKeyPress = ::this.handleFilterKeyPress + this.handleHighlight = ::this.handleHighlight } static defaultProps = { @@ -24,6 +34,7 @@ class Dropdown extends Component { buttonSize: 'btn-sm', buttonColor: 'btn-info', menuWidth: '100%', + useAutoComplete: false, } handleClickOutside() { @@ -42,10 +53,21 @@ class Dropdown extends Component { this.props.onChoose(item) } + handleHighlight(itemIndex) { + this.setState({highlightedItemIndex: itemIndex}) + } + toggleMenu(e) { if (e) { e.stopPropagation() } + if (!this.state.isOpen) { + this.setState({ + searchTerm: '', + filteredItems: this.props.items, + highlightedItemIndex: null, + }) + } this.setState({isOpen: !this.state.isOpen}) } @@ -54,21 +76,94 @@ class Dropdown extends Component { action.handler(item) } + handleFilterKeyPress(e) { + const {filteredItems, highlightedItemIndex} = this.state + + if (e.key === 'Enter' && filteredItems.length) { + this.setState({isOpen: false}) + this.props.onChoose(filteredItems[highlightedItemIndex]) + } + if (e.key === 'Escape') { + this.setState({isOpen: false}) + } + if (e.key === 'ArrowUp' && highlightedItemIndex > 0) { + this.setState({highlightedItemIndex: highlightedItemIndex - 1}) + } + if (e.key === 'ArrowDown') { + if (highlightedItemIndex < filteredItems.length - 1) { + this.setState({highlightedItemIndex: highlightedItemIndex + 1}) + } + if (highlightedItemIndex === null && filteredItems.length) { + this.setState({highlightedItemIndex: 0}) + } + } + } + + handleFilterChange(e) { + if (e.target.value === null || e.target.value === '') { + this.setState({ + searchTerm: '', + filteredItems: this.props.items, + highlightedItemIndex: null, + }) + } else { + this.setState({searchTerm: e.target.value}, () => + this.applyFilter(this.state.searchTerm) + ) + } + } + + applyFilter(searchTerm) { + const {items} = this.props + const filterText = searchTerm.toLowerCase() + const matchingItems = items.filter(item => + item.text.toLowerCase().includes(filterText) + ) + + this.setState({ + filteredItems: matchingItems, + highlightedItemIndex: 0, + }) + } + renderShortMenu() { - const {actions, addNew, items, menuWidth, menuLabel} = this.props + const { + actions, + addNew, + items, + menuWidth, + menuLabel, + useAutoComplete, + selected, + } = this.props + const {filteredItems, highlightedItemIndex} = this.state + const menuItems = useAutoComplete ? filteredItems : items + return ( -
) } } -const {arrayOf, shape, string, func} = PropTypes +const {arrayOf, bool, shape, string, func} = PropTypes Dropdown.propTypes = { actions: arrayOf( @@ -218,6 +358,7 @@ Dropdown.propTypes = { buttonColor: string, menuWidth: string, menuLabel: string, + useAutoComplete: bool, } export default OnClickOutside(Dropdown) diff --git a/ui/src/style/components/dropdown.scss b/ui/src/style/components/dropdown.scss index 4e96bc0b40..29543cedd7 100644 --- a/ui/src/style/components/dropdown.scss +++ b/ui/src/style/components/dropdown.scss @@ -12,30 +12,16 @@ $dropdown-menu-max-height: 270px; Generic width modifiers Use instead of creating new classes if possible */ -.dropdown .dropdown-toggle { +.dropdown .dropdown-toggle, +.dropdown .dropdown-autocomplete { width: 120px; /* Default width */ } .dropdown { - &-80 .dropdown-toggle {width: 80px;} - &-90 .dropdown-toggle {width: 90px;} - &-100 .dropdown-toggle {width: 100px;} - &-110 .dropdown-toggle {width: 110px;} - &-120 .dropdown-toggle {width: 120px;} - &-130 .dropdown-toggle {width: 130px;} - &-140 .dropdown-toggle {width: 140px;} - &-150 .dropdown-toggle {width: 150px;} - &-160 .dropdown-toggle {width: 160px;} - &-170 .dropdown-toggle {width: 170px;} - &-180 .dropdown-toggle {width: 180px;} - &-190 .dropdown-toggle {width: 190px;} - &-200 .dropdown-toggle {width: 200px;} - &-210 .dropdown-toggle {width: 210px;} - &-220 .dropdown-toggle {width: 220px;} - &-230 .dropdown-toggle {width: 230px;} - &-240 .dropdown-toggle {width: 240px;} - &-250 .dropdown-toggle {width: 250px;} + @for $i from 8 through 30 { + &-#{$i * 10} .dropdown-toggle, + &-#{$i * 10} .dropdown-autocomplete { width: #{$i * 10}px; } + } } - .dropdown-toggle { position: relative; text-align: left; @@ -65,7 +51,54 @@ $dropdown-menu-max-height: 270px; .dropdown .dropdown-toggle.btn-xs { height: 22px !important; line-height: 22px !important; - padding: 0 9px !important; + padding: 0 9px; +} + +/* + AutoComplete Field + ---------------------------------------------- +*/ +.dropdown-autocomplete { + position: relative; + padding: 0 !important; + + &.btn-xs {height: 22px;} + &.btn-sm {height: 30px;} + &.btn-md {height: 36px;} + &.btn-lg {height: 50px;} +} +.dropdown-autocomplete--input { + position: absolute; + width: 100%; + height: 100%; + outline: none; + background-color: transparent; + border: 0; + color: $g20-white; + padding: 0; + font-weight: 500; + + .btn-xs & {padding: 0 18px 0 9px; font-size: 12px;} + .btn-sm & {padding: 0 18px 0 9px; font-size: 13px;} + .btn-md & {padding: 0 34px 0 17px; font-size: 14px;} + .btn-lg & {padding: 0 48px 0 24px; font-size: 18px;} + + &::-webkit-input-placeholder { color: rgba(255,255,255,0.5); font-weight: 500 !important; } + &::-moz-placeholder { color: rgba(255,255,255,0.5); font-weight: 500 !important; } + &:-ms-input-placeholder { color: rgba(255,255,255,0.5); font-weight: 500 !important; } + &:-moz-placeholder { color: rgba(255,255,255,0.5); font-weight: 500 !important; } + + &:focus { + color: $g20-white; + } +} +.dropdown-empty { + padding: 7px 9px; + font-size: 13px; + color: rgba(255,255,255,0.4); + font-weight: 500; + line-height: 15px; + @include no-user-select(); } /* @@ -86,6 +119,9 @@ $dropdown-menu-max-height: 270px; position: relative; width: 100%; + &.active { + @include gradient-h($c-sapphire, $c-pool); + } &:hover { @include gradient-h($c-laser, $c-pool); } @@ -99,7 +135,7 @@ $dropdown-menu-max-height: 270px; padding: 7px 9px; font-size: 13px; line-height: 15px; - font-weight: 500; + font-weight: 500 !important; color: $c-yeti !important; background-color: transparent; transition: @@ -118,6 +154,16 @@ $dropdown-menu-max-height: 270px; @include gradient-h($c-ocean, $c-pool); } } + li.dropdown-item.highlight { + &, &:hover { + @include gradient-h($c-laser, $c-pool); + } + > a { + background: none; + background-color: transparent; + color: $g20-white; + } + } } .dropdown.dropdown-kapacitor .dropdown-toggle { color: $c-rainforest !important; @@ -137,6 +183,16 @@ $dropdown-menu-max-height: 270px; color: $g20-white !important; } } + li.dropdown-item.highlight { + &, &:hover { + @include gradient-h($c-laser, $c-rainforest); + } + > a { + background: none; + background-color: transparent; + color: $g20-white; + } + } } .dropdown.dropdown-chronograf .dropdown-menu { @include custom-scrollbar($c-comet, $c-potassium); @@ -151,8 +207,36 @@ $dropdown-menu-max-height: 270px; color: $g20-white !important; } } + li.dropdown-item.highlight { + &, &:hover { + @include gradient-h($c-laser, $c-comet); + } + > a { + background: none; + background-color: transparent; + color: $g20-white; + } + } } +/* + Dropdown Menu (only js highlighting, works with autocomplete feature) + ---------------------------------------------- +*/ +.dropdown-menu.dropdown-menu--no-highlight { + li.dropdown-item { + + &:hover { + background: none; + background-color: transparent; + } + } + li.dropdown-item.highlight { + &, &:hover { + @include gradient-h($c-laser, $c-pool); + } + } +} /* Dropdown Header ---------------------------------------------- @@ -256,4 +340,4 @@ $dropdown-menu-max-height: 270px; background-color: $g2-kevlar !important; border-color: $c-pool !important; box-shadow: 0 0 6px 0px $c-pool !important -} \ No newline at end of file +} diff --git a/ui/src/style/components/template-control-bar.scss b/ui/src/style/components/template-control-bar.scss index f6e3b70dc7..b8ad33f61d 100644 --- a/ui/src/style/components/template-control-bar.scss +++ b/ui/src/style/components/template-control-bar.scss @@ -57,10 +57,13 @@ $template-control--min-height: 52px; } .dropdown-menu { @include gradient-h($c-star,$c-pool); - @include custom-scrollbar-round($c-pool,$c-laser); li.dropdown-item { - &:hover {@include gradient-h($c-comet,$c-pool);} + &, &:hover { + background: none; + background-color: transparent; + } + &.active {@include gradient-h($c-amethyst,$c-pool);} } li.dropdown-item > a { &, &:focus {background: none;} @@ -68,6 +71,14 @@ $template-control--min-height: 52px; font-size: 12px; font-family: $code-font; } + li.dropdown-item.highlight { + &, &:hover {@include gradient-h($c-comet,$c-pool);} + > a { + color: $g20-white; + background: none; + background-color: transparent; + } + } } } .template-control--label {