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';
import LoginBackground from './components/Login/LoginBackground';
import { SearchResult } from './components/search/SearchResult';
import UserPicker from './components/UserPicker/UserPicker';
import { TagFilter } from './components/TagFilter/TagFilter';
export function registerAngularDirectives() {
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
......@@ -13,4 +14,9 @@ export function registerAngularDirectives() {
react2AngularDirective('loginBackground', LoginBackground, []);
react2AngularDirective('searchResult', SearchResult, []);
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 @@
spellcheck='false'
ng-change="ctrl.search()"
ng-blur="ctrl.searchInputBlur()"
/>
/>
<div class="search-field-spacer"></div>
</div>
......@@ -31,28 +31,18 @@
</div>
<div class="search-dropdown__col_2">
<!-- <div class="search&#45;filter&#45;box"> -->
<!-- <div class="search&#45;filter&#45;box__header"> -->
<!-- <i class="fa fa&#45;filter"></i> -->
<!-- Filter by: -->
<!-- <a class="pointer pull&#45;right small"> -->
<!-- <i class="fa fa&#45;remove"></i> Clear -->
<!-- </a> -->
<!-- </div> -->
<!-- -->
<!-- <div class="gf&#45;form"> -->
<!-- <folder&#45;picker initial&#45;title="ctrl.initialFolderFilterTitle" -->
<!-- on&#45;change="ctrl.onFolderChange($folder)" -->
<!-- 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" ng-click="ctrl.onFilterboxClick()">
<div class="search-filter-box__header">
<i class="fa fa-filter"></i>
Filter by:
<a class="pointer pull-right small" ng-click="ctrl.clearSearchFilter()">
<i class="fa fa-remove"></i> Clear
</a>
</div>
<tag-filter tags="ctrl.query.tag" tagOptions="ctrl.getTags" onSelect="ctrl.onTagSelect">
</tag-filter>
</div>
<div class="search-filter-box">
<a href="dashboard/new" class="search-filter-box-link">
......
......@@ -22,6 +22,8 @@ export class SearchCtrl {
appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope);
this.initialFolderFilterTitle = 'All';
this.getTags = this.getTags.bind(this);
this.onTagSelect = this.onTagSelect.bind(this);
}
closeSearch() {
......@@ -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) {
if (this.results.length === 0) {
return;
......@@ -160,7 +179,6 @@ export class SearchCtrl {
if (_.indexOf(this.query.tag, tag) === -1) {
this.query.tag.push(tag);
this.search();
this.giveSearchFocus = this.giveSearchFocus + 1;
}
}
......@@ -173,10 +191,17 @@ export class SearchCtrl {
}
getTags() {
return this.searchSrv.getDashboardTags().then(results => {
this.results = results;
this.giveSearchFocus = this.giveSearchFocus + 1;
});
return this.searchSrv.getDashboardTags();
}
onTagSelect(newTags) {
this.query.tag = _.map(newTags, tag => tag.value);
this.search();
}
clearSearchFilter() {
this.query.tag = [];
this.search();
}
showStarred() {
......
import angular from 'angular';
import $ from 'jquery';
import coreModule from '../core_module';
import tags from 'app/core/utils/tags';
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) {
var hash = djb2(name.toLowerCase());
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)];
const { color, borderColor } = tags.getTagColorsFromName(name);
element.css('background-color', color);
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 {
.highlight-search-match {
background: transparent;
color: $yellow;
padding: 0;
}
......@@ -26,18 +26,41 @@ $select-menu-box-shadow: $menu-dropdown-shadow;
@include box-shadow($shadow);
}
// react-select tweaks
.gf-form-input--form-dropdown {
padding: 0;
border: 0;
overflow: visible;
.Select-placeholder {
color: $gray-4;
color: $input-color-placeholder;
}
> .Select-control {
@include select-control();
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 {
......@@ -50,6 +73,11 @@ $select-menu-box-shadow: $menu-dropdown-shadow;
@include select-control-focus();
}
&.is-focused:not(.is-open) > .Select-control {
background-color: $input-bg;
@include select-control-focus();
}
.Select-menu-outer {
border: 0;
width: auto;
......
......@@ -51,6 +51,11 @@ $input-border: 1px solid $input-border-color;
color: $text-muted;
}
}
.Select--multi .Select-multi-value-wrapper,
.Select-placeholder {
padding-left: 30px;
}
}
.gf-form-disabled {
......
......@@ -26,7 +26,7 @@
box-shadow: $navbarShadow;
position: relative;
input {
& > input {
max-width: 653px;
//padding: 0.5rem 1.5rem 0.5rem 0;
padding: 1rem 1rem 0.75rem 1rem;
......@@ -38,6 +38,13 @@
background-color: $navbarButtonBackground;
flex-grow: 10;
}
// .tag-filter {
// .Select-control {
// width: 300px;
// background-color: $navbarBackground;
// }
// }
}
.search-field-spacer {
......@@ -67,13 +74,16 @@
flex-grow: 1;
height: 100%;
padding-top: 16px;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.search-filter-box {
background: $search-filter-box-bg;
border-radius: 2px;
padding: $spacer*1.5;
max-width: 340px;
min-width: 340px;
margin-bottom: $spacer * 1.5;
margin-left: $spacer * 1.5;
}
......
......@@ -21,7 +21,7 @@
border-radius: 3px;
text-shadow: none;
font-size: 13px;
padding: 2px 6px;
padding: 3px 6px 1px 6px;
border-width: 1px;
border-style: solid;
box-shadow: 0 0 1px rgba($white, 0.2);
......
......@@ -21,16 +21,15 @@
margin-right: 2px;
color: white;
[data-role="remove"] {
[data-role='remove'] {
margin-left: 8px;
cursor: pointer;
&::after {
content: "x";
content: 'x';
padding: 0px 2px;
}
&:hover {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
&:active {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}
......@@ -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