Commit 11ba8070 by Torkel Ödegaard Committed by GitHub

Tag filters in search (#10521)

* tag filter: initial react component

* dashboard: move tag filter to filterbox

* tag filter: customize value rendering

* tag filter: get color from name

* tag filter:  custom option renderer

* tag filter: mode with tags in different container

* tag filter: refactor

* refactoring PR #10519

* tag filter: refactor of PR #10521
parent 42d73080
...@@ -5,6 +5,7 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; ...@@ -5,6 +5,7 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
import LoginBackground from './components/Login/LoginBackground'; import LoginBackground from './components/Login/LoginBackground';
import { SearchResult } from './components/search/SearchResult'; import { SearchResult } from './components/search/SearchResult';
import UserPicker from './components/UserPicker/UserPicker'; import UserPicker from './components/UserPicker/UserPicker';
import { TagFilter } from './components/TagFilter/TagFilter';
export function registerAngularDirectives() { export function registerAngularDirectives() {
react2AngularDirective('passwordStrength', PasswordStrength, ['password']); react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
...@@ -13,4 +14,9 @@ export function registerAngularDirectives() { ...@@ -13,4 +14,9 @@ export function registerAngularDirectives() {
react2AngularDirective('loginBackground', LoginBackground, []); react2AngularDirective('loginBackground', LoginBackground, []);
react2AngularDirective('searchResult', SearchResult, []); react2AngularDirective('searchResult', SearchResult, []);
react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'teamId', 'refreshList']); react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'teamId', 'refreshList']);
react2AngularDirective('tagFilter', TagFilter, [
'tags',
['onSelect', { watchDepth: 'reference' }],
['tagOptions', { watchDepth: 'reference' }],
]);
} }
import React from 'react';
import tags from 'app/core/utils/tags';
export interface IProps {
label: string;
removeIcon: boolean;
count: number;
onClick: any;
}
export class TagBadge extends React.Component<IProps, any> {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick(event) {
this.props.onClick(event);
}
render() {
const { label, removeIcon, count } = this.props;
const { color, borderColor } = tags.getTagColorsFromName(label);
const tagStyle = {
backgroundColor: color,
borderColor: borderColor,
};
const countLabel = count !== 0 && <span className="tag-count-label">{`(${count})`}</span>;
return (
<span className={`label label-tag`} onClick={this.onClick} style={tagStyle}>
{removeIcon && <i className="fa fa-remove" />}
{label} {countLabel}
</span>
);
}
}
import _ from 'lodash';
import React from 'react';
import { Async } from 'react-select';
import { TagValue } from './TagValue';
import { TagOption } from './TagOption';
export interface IProps {
tags: string[];
tagOptions: () => any;
onSelect: (tag: string) => void;
}
export class TagFilter extends React.Component<IProps, any> {
inlineTags: boolean;
constructor(props) {
super(props);
this.searchTags = this.searchTags.bind(this);
this.onChange = this.onChange.bind(this);
this.onTagRemove = this.onTagRemove.bind(this);
}
searchTags(query) {
return this.props.tagOptions().then(options => {
const tags = _.map(options, tagOption => {
return { value: tagOption.term, label: tagOption.term, count: tagOption.count };
});
return { options: tags };
});
}
onChange(newTags) {
this.props.onSelect(newTags);
}
onTagRemove(tag) {
let newTags = _.without(this.props.tags, tag.label);
newTags = _.map(newTags, tag => {
return { value: tag };
});
this.props.onSelect(newTags);
}
render() {
let selectOptions = {
loadOptions: this.searchTags,
onChange: this.onChange,
value: this.props.tags,
multi: true,
className: 'gf-form-input gf-form-input--form-dropdown',
placeholder: 'Tags',
loadingPlaceholder: 'Loading...',
noResultsText: 'No tags found',
optionComponent: TagOption,
};
selectOptions['valueComponent'] = TagValue;
return (
<div className="gf-form gf-form--has-input-icon gf-form--grow">
<div className="tag-filter">
<Async {...selectOptions} />
</div>
<i className="gf-form-input-icon fa fa-tag" />
</div>
);
}
}
import React from 'react';
import { TagBadge } from './TagBadge';
export interface IProps {
onSelect: any;
onFocus: any;
option: any;
isFocused: any;
className: any;
}
export class TagOption extends React.Component<IProps, any> {
constructor(props) {
super(props);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
}
handleMouseDown(event) {
event.preventDefault();
event.stopPropagation();
this.props.onSelect(this.props.option, event);
}
handleMouseEnter(event) {
this.props.onFocus(this.props.option, event);
}
handleMouseMove(event) {
if (this.props.isFocused) {
return;
}
this.props.onFocus(this.props.option, event);
}
render() {
const { option, className } = this.props;
return (
<button
onMouseDown={this.handleMouseDown}
onMouseEnter={this.handleMouseEnter}
onMouseMove={this.handleMouseMove}
title={option.title}
className={`tag-filter-option btn btn-link ${className || ''}`}
>
<TagBadge label={option.label} removeIcon={false} count={option.count} onClick={this.handleMouseDown} />
</button>
);
}
}
import React from 'react';
import { TagBadge } from './TagBadge';
export interface IProps {
value: any;
className: any;
onClick: any;
onRemove: any;
}
export class TagValue extends React.Component<IProps, any> {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
}
onClick(event) {
this.props.onRemove(this.props.value, event);
}
render() {
const { value } = this.props;
return <TagBadge label={value.label} removeIcon={true} count={0} onClick={this.onClick} />;
}
}
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
spellcheck='false' spellcheck='false'
ng-change="ctrl.search()" ng-change="ctrl.search()"
ng-blur="ctrl.searchInputBlur()" ng-blur="ctrl.searchInputBlur()"
/> />
<div class="search-field-spacer"></div> <div class="search-field-spacer"></div>
</div> </div>
...@@ -31,28 +31,18 @@ ...@@ -31,28 +31,18 @@
</div> </div>
<div class="search-dropdown__col_2"> <div class="search-dropdown__col_2">
<!-- <div class="search&#45;filter&#45;box"> --> <div class="search-filter-box" ng-click="ctrl.onFilterboxClick()">
<!-- <div class="search&#45;filter&#45;box__header"> --> <div class="search-filter-box__header">
<!-- <i class="fa fa&#45;filter"></i> --> <i class="fa fa-filter"></i>
<!-- Filter by: --> Filter by:
<!-- <a class="pointer pull&#45;right small"> --> <a class="pointer pull-right small" ng-click="ctrl.clearSearchFilter()">
<!-- <i class="fa fa&#45;remove"></i> Clear --> <i class="fa fa-remove"></i> Clear
<!-- </a> --> </a>
<!-- </div> --> </div>
<!-- -->
<!-- <div class="gf&#45;form"> --> <tag-filter tags="ctrl.query.tag" tagOptions="ctrl.getTags" onSelect="ctrl.onTagSelect">
<!-- <folder&#45;picker initial&#45;title="ctrl.initialFolderFilterTitle" --> </tag-filter>
<!-- on&#45;change="ctrl.onFolderChange($folder)" --> </div>
<!-- label&#45;class="width&#45;4"> -->
<!-- </folder&#45;picker> -->
<!-- </div> -->
<!-- -->
<!-- <div class="gf&#45;form"> -->
<!-- <label class="gf&#45;form&#45;label width&#45;4">Tags</label> -->
<!-- <bootstrap&#45;tagsinput ng&#45;model="ctrl.dashboard.tags" tagclass="label label&#45;tag" placeholder="add tags"> -->
<!-- </bootstrap&#45;tagsinput> -->
<!-- </div> -->
<!-- </div> -->
<div class="search-filter-box"> <div class="search-filter-box">
<a href="dashboard/new" class="search-filter-box-link"> <a href="dashboard/new" class="search-filter-box-link">
......
...@@ -22,6 +22,8 @@ export class SearchCtrl { ...@@ -22,6 +22,8 @@ export class SearchCtrl {
appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope); appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope);
this.initialFolderFilterTitle = 'All'; this.initialFolderFilterTitle = 'All';
this.getTags = this.getTags.bind(this);
this.onTagSelect = this.onTagSelect.bind(this);
} }
closeSearch() { closeSearch() {
...@@ -88,6 +90,23 @@ export class SearchCtrl { ...@@ -88,6 +90,23 @@ export class SearchCtrl {
} }
} }
searchInputBlur() {
this.search();
}
onFilterboxClick() {
this.giveSearchFocus = 0;
this.preventClose();
}
preventClose() {
this.ignoreClose = true;
this.$timeout(() => {
this.ignoreClose = false;
}, 100);
}
moveSelection(direction) { moveSelection(direction) {
if (this.results.length === 0) { if (this.results.length === 0) {
return; return;
...@@ -160,7 +179,6 @@ export class SearchCtrl { ...@@ -160,7 +179,6 @@ export class SearchCtrl {
if (_.indexOf(this.query.tag, tag) === -1) { if (_.indexOf(this.query.tag, tag) === -1) {
this.query.tag.push(tag); this.query.tag.push(tag);
this.search(); this.search();
this.giveSearchFocus = this.giveSearchFocus + 1;
} }
} }
...@@ -173,10 +191,17 @@ export class SearchCtrl { ...@@ -173,10 +191,17 @@ export class SearchCtrl {
} }
getTags() { getTags() {
return this.searchSrv.getDashboardTags().then(results => { return this.searchSrv.getDashboardTags();
this.results = results; }
this.giveSearchFocus = this.giveSearchFocus + 1;
}); onTagSelect(newTags) {
this.query.tag = _.map(newTags, tag => tag.value);
this.search();
}
clearSearchFilter() {
this.query.tag = [];
this.search();
} }
showStarred() { showStarred() {
......
import angular from 'angular'; import angular from 'angular';
import $ from 'jquery'; import $ from 'jquery';
import coreModule from '../core_module'; import coreModule from '../core_module';
import tags from 'app/core/utils/tags';
import 'vendor/tagsinput/bootstrap-tagsinput.js'; import 'vendor/tagsinput/bootstrap-tagsinput.js';
function djb2(str) {
var hash = 5381;
for (var i = 0; i < str.length; i++) {
hash = (hash << 5) + hash + str.charCodeAt(i); /* hash * 33 + c */
}
return hash;
}
function setColor(name, element) { function setColor(name, element) {
var hash = djb2(name.toLowerCase()); const { color, borderColor } = tags.getTagColorsFromName(name);
var colors = [
'#E24D42',
'#1F78C1',
'#BA43A9',
'#705DA0',
'#466803',
'#508642',
'#447EBC',
'#C15C17',
'#890F02',
'#757575',
'#0A437C',
'#6D1F62',
'#584477',
'#629E51',
'#2F4F4F',
'#BF1B00',
'#806EB7',
'#8a2eb8',
'#699e00',
'#000000',
'#3F6833',
'#2F575E',
'#99440A',
'#E0752D',
'#0E4AB4',
'#58140C',
'#052B51',
'#511749',
'#3F2B5B',
];
var borderColors = [
'#FF7368',
'#459EE7',
'#E069CF',
'#9683C6',
'#6C8E29',
'#76AC68',
'#6AA4E2',
'#E7823D',
'#AF3528',
'#9B9B9B',
'#3069A2',
'#934588',
'#7E6A9D',
'#88C477',
'#557575',
'#E54126',
'#A694DD',
'#B054DE',
'#8FC426',
'#262626',
'#658E59',
'#557D84',
'#BF6A30',
'#FF9B53',
'#3470DA',
'#7E3A32',
'#2B5177',
'#773D6F',
'#655181',
];
var color = colors[Math.abs(hash % colors.length)];
var borderColor = borderColors[Math.abs(hash % borderColors.length)];
element.css('background-color', color); element.css('background-color', color);
element.css('border-color', borderColor); element.css('border-color', borderColor);
} }
......
const TAG_COLORS = [
'#E24D42',
'#1F78C1',
'#BA43A9',
'#705DA0',
'#466803',
'#508642',
'#447EBC',
'#C15C17',
'#890F02',
'#757575',
'#0A437C',
'#6D1F62',
'#584477',
'#629E51',
'#2F4F4F',
'#BF1B00',
'#806EB7',
'#8a2eb8',
'#699e00',
'#000000',
'#3F6833',
'#2F575E',
'#99440A',
'#E0752D',
'#0E4AB4',
'#58140C',
'#052B51',
'#511749',
'#3F2B5B',
];
const TAG_BORDER_COLORS = [
'#FF7368',
'#459EE7',
'#E069CF',
'#9683C6',
'#6C8E29',
'#76AC68',
'#6AA4E2',
'#E7823D',
'#AF3528',
'#9B9B9B',
'#3069A2',
'#934588',
'#7E6A9D',
'#88C477',
'#557575',
'#E54126',
'#A694DD',
'#B054DE',
'#8FC426',
'#262626',
'#658E59',
'#557D84',
'#BF6A30',
'#FF9B53',
'#3470DA',
'#7E3A32',
'#2B5177',
'#773D6F',
'#655181',
];
/**
* Returns tag badge background and border colors based on hashed tag name.
* @param name tag name
*/
export function getTagColorsFromName(name: string): { color: string; borderColor: string } {
let hash = djb2(name.toLowerCase());
let color = TAG_COLORS[Math.abs(hash % TAG_COLORS.length)];
let borderColor = TAG_BORDER_COLORS[Math.abs(hash % TAG_BORDER_COLORS.length)];
return { color, borderColor };
}
function djb2(str) {
let hash = 5381;
for (var i = 0; i < str.length; i++) {
hash = (hash << 5) + hash + str.charCodeAt(i); /* hash * 33 + c */
}
return hash;
}
export default {
getTagColorsFromName,
};
...@@ -413,4 +413,5 @@ a.external-link { ...@@ -413,4 +413,5 @@ a.external-link {
.highlight-search-match { .highlight-search-match {
background: transparent; background: transparent;
color: $yellow; color: $yellow;
padding: 0;
} }
...@@ -26,18 +26,41 @@ $select-menu-box-shadow: $menu-dropdown-shadow; ...@@ -26,18 +26,41 @@ $select-menu-box-shadow: $menu-dropdown-shadow;
@include box-shadow($shadow); @include box-shadow($shadow);
} }
// react-select tweaks
.gf-form-input--form-dropdown { .gf-form-input--form-dropdown {
padding: 0; padding: 0;
border: 0; border: 0;
overflow: visible; overflow: visible;
.Select-placeholder { .Select-placeholder {
color: $gray-4; color: $input-color-placeholder;
} }
> .Select-control { > .Select-control {
@include select-control(); @include select-control();
border-color: $dark-3; border-color: $dark-3;
input {
min-width: 1rem;
}
.Select-clear,
.Select-arrow {
margin-right: 8px;
}
.Select-value {
display: inline-block;
padding: 2px 4px;
font-size: $font-size-base * 0.846;
font-weight: bold;
line-height: 14px; // ensure proper line-height if floated
color: $white;
vertical-align: baseline;
white-space: nowrap;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
background-color: $gray-1;
}
} }
&.is-open > .Select-control { &.is-open > .Select-control {
...@@ -50,6 +73,11 @@ $select-menu-box-shadow: $menu-dropdown-shadow; ...@@ -50,6 +73,11 @@ $select-menu-box-shadow: $menu-dropdown-shadow;
@include select-control-focus(); @include select-control-focus();
} }
&.is-focused:not(.is-open) > .Select-control {
background-color: $input-bg;
@include select-control-focus();
}
.Select-menu-outer { .Select-menu-outer {
border: 0; border: 0;
width: auto; width: auto;
......
...@@ -51,6 +51,11 @@ $input-border: 1px solid $input-border-color; ...@@ -51,6 +51,11 @@ $input-border: 1px solid $input-border-color;
color: $text-muted; color: $text-muted;
} }
} }
.Select--multi .Select-multi-value-wrapper,
.Select-placeholder {
padding-left: 30px;
}
} }
.gf-form-disabled { .gf-form-disabled {
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
box-shadow: $navbarShadow; box-shadow: $navbarShadow;
position: relative; position: relative;
input { & > input {
max-width: 653px; max-width: 653px;
//padding: 0.5rem 1.5rem 0.5rem 0; //padding: 0.5rem 1.5rem 0.5rem 0;
padding: 1rem 1rem 0.75rem 1rem; padding: 1rem 1rem 0.75rem 1rem;
...@@ -38,6 +38,13 @@ ...@@ -38,6 +38,13 @@
background-color: $navbarButtonBackground; background-color: $navbarButtonBackground;
flex-grow: 10; flex-grow: 10;
} }
// .tag-filter {
// .Select-control {
// width: 300px;
// background-color: $navbarBackground;
// }
// }
} }
.search-field-spacer { .search-field-spacer {
...@@ -67,13 +74,16 @@ ...@@ -67,13 +74,16 @@
flex-grow: 1; flex-grow: 1;
height: 100%; height: 100%;
padding-top: 16px; padding-top: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
} }
.search-filter-box { .search-filter-box {
background: $search-filter-box-bg; background: $search-filter-box-bg;
border-radius: 2px; border-radius: 2px;
padding: $spacer*1.5; padding: $spacer*1.5;
max-width: 340px; min-width: 340px;
margin-bottom: $spacer * 1.5; margin-bottom: $spacer * 1.5;
margin-left: $spacer * 1.5; margin-left: $spacer * 1.5;
} }
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
border-radius: 3px; border-radius: 3px;
text-shadow: none; text-shadow: none;
font-size: 13px; font-size: 13px;
padding: 2px 6px; padding: 3px 6px 1px 6px;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
box-shadow: 0 0 1px rgba($white, 0.2); box-shadow: 0 0 1px rgba($white, 0.2);
......
...@@ -21,16 +21,15 @@ ...@@ -21,16 +21,15 @@
margin-right: 2px; margin-right: 2px;
color: white; color: white;
[data-role="remove"] { [data-role='remove'] {
margin-left: 8px; margin-left: 8px;
cursor: pointer; cursor: pointer;
&::after { &::after {
content: "x"; content: 'x';
padding: 0px 2px; padding: 0px 2px;
} }
&:hover { &:hover {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
0 1px 2px rgba(0, 0, 0, 0.05);
&:active { &:active {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
} }
...@@ -38,3 +37,51 @@ ...@@ -38,3 +37,51 @@
} }
} }
} }
.tag-filter {
line-height: 22px;
flex-grow: 1;
.label-tag {
margin-left: 6px;
font-size: 11px;
cursor: pointer;
.fa.fa-remove {
margin-right: 3px;
}
}
.tag-filter-option {
position: relative;
text-align: left;
width: 100%;
display: block;
border-radius: 0;
}
.tag-count-label {
margin-left: 3px;
}
.gf-form-input--form-dropdown {
.Select-menu-outer {
border: 0;
width: 100%;
}
}
}
.tag-filter-values {
display: inline;
.label-tag {
margin: 6px 6px 0px 0px;
font-size: 11px;
cursor: pointer;
.fa.fa-remove {
margin-right: 3px;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment