From 612944589ec47853e38ec174da46db438b08c11e Mon Sep 17 00:00:00 2001 From: Mike Cohen Date: Wed, 10 Dec 2025 12:50:37 +1000 Subject: [PATCH 1/3] Reworked client label button to separate removal action. Users intuitively expect clicking the label to search by label not to remove the label. This new UI design separates the button into two halves - the left half refines the search to the label but the right part removes the label --- gui/velociraptor/src/App.jsx | 9 ++++---- .../src/components/clients/clients-list.jsx | 22 +++++++++++++------ .../src/components/clients/search.jsx | 11 ++++++++-- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/gui/velociraptor/src/App.jsx b/gui/velociraptor/src/App.jsx index c58572e11cc..fce7ca2a1c9 100644 --- a/gui/velociraptor/src/App.jsx +++ b/gui/velociraptor/src/App.jsx @@ -139,10 +139,11 @@ class App extends Component { - - + + + { + this.props.setSearch("label:" + label); + } + isSelected = c=>{ return _.includes(this.state.selected, c.client_id); } @@ -693,14 +697,18 @@ class VeloClientList extends Component { {c && c.os_info && c.os_info.fqdn} {c && c.os_info && c.os_info.release} {_.map(c.labels, (label, idx)=>{ - return + ; + + ; })} ); })} diff --git a/gui/velociraptor/src/components/clients/search.jsx b/gui/velociraptor/src/components/clients/search.jsx index ed65a6fd2d1..ae6bfe2fb48 100644 --- a/gui/velociraptor/src/components/clients/search.jsx +++ b/gui/velociraptor/src/components/clients/search.jsx @@ -34,10 +34,18 @@ class VeloClientSearch extends Component { let query = this.props.match && this.props.match.params && this.props.match.params.query; if (query && query !== this.state.query) { - this.this.setState({query: query}); + this.setState({query: query}); }; }; + componentDidUpdate = (prevProps, prevState, rootNode) => { + let query = this.props.match && this.props.match.params && + this.props.match.params.query; + if (query && query !== this.state.query) { + this.setState({query: query}); + }; + } + componentWillUnmount() { this.source.cancel("unmounted"); } @@ -150,5 +158,4 @@ class VeloClientSearch extends Component { } }; - export default withRouter(VeloClientSearch); From 4ac1139aa03242f8d3b8939a49daabce05bceeff Mon Sep 17 00:00:00 2001 From: Mike Cohen Date: Wed, 10 Dec 2025 13:03:01 +1000 Subject: [PATCH 2/3] Do not use the router to get the search term The search box is used outside the route too. --- gui/velociraptor/src/App.jsx | 7 ++---- .../src/components/clients/search.jsx | 25 +++++++++---------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/gui/velociraptor/src/App.jsx b/gui/velociraptor/src/App.jsx index fce7ca2a1c9..fdbe9096fc1 100644 --- a/gui/velociraptor/src/App.jsx +++ b/gui/velociraptor/src/App.jsx @@ -139,11 +139,8 @@ class App extends Component { - - - + { this.source = CancelToken.source(); - let query = this.props.match && this.props.match.params && - this.props.match.params.query; + let query = this.getQueryFromLocation(); if (query && query !== this.state.query) { this.setState({query: query}); }; }; componentDidUpdate = (prevProps, prevState, rootNode) => { - let query = this.props.match && this.props.match.params && - this.props.match.params.query; + let query = this.getQueryFromLocation(); if (query && query !== this.state.query) { - this.setState({query: query}); + this.setQuery(query); }; } @@ -56,6 +49,14 @@ class VeloClientSearch extends Component { options: [], } + getQueryFromLocation = ()=>{ + let hash = window.location.hash || ""; + if(hash.startsWith("#/search/")) { + return hash.substring(9); + }; + return ""; + } + showAll = () => { this.setState({query: "all"}); this.props.setSearch("all"); @@ -157,5 +158,3 @@ class VeloClientSearch extends Component { ); } }; - -export default withRouter(VeloClientSearch); From 0cb6d10df221074bac6dbc989d6099bce1e1530f Mon Sep 17 00:00:00 2001 From: Mike Cohen Date: Wed, 10 Dec 2025 13:41:50 +1000 Subject: [PATCH 3/3] Make edit box uncontrolled so we can update it --- gui/velociraptor/src/App.jsx | 4 +++ .../src/components/clients/search.jsx | 33 ++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/gui/velociraptor/src/App.jsx b/gui/velociraptor/src/App.jsx index fdbe9096fc1..4197524050a 100644 --- a/gui/velociraptor/src/App.jsx +++ b/gui/velociraptor/src/App.jsx @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import './css/App.css'; import qs from "qs"; +import _ from 'lodash'; import PropTypes from 'prop-types'; import VeloNavigator from './components/sidebar/navigator.jsx'; @@ -96,6 +97,9 @@ class App extends Component { }; setClientSearch = (query) => { + if (!_.isString(query)) { + return; + } let now = new Date(); this.setState({query: query, query_version: now.getTime()}); this.props.history.push('/search/' + (query || "all")); diff --git a/gui/velociraptor/src/components/clients/search.jsx b/gui/velociraptor/src/components/clients/search.jsx index b9cd65822fc..d01725eb369 100644 --- a/gui/velociraptor/src/components/clients/search.jsx +++ b/gui/velociraptor/src/components/clients/search.jsx @@ -1,4 +1,5 @@ import "./search.css"; +import _ from 'lodash'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; @@ -46,6 +47,14 @@ export default class VeloClientSearch extends Component { state = { // query used to update suggestions. query: "", + + // When the user begins editing the query string we set the + // new query here. When the user submits the query, we clear + // this and set the query in the upstream component. + + // When the edit box is editing, we block automatic updates of + // the query content from the URL. + pending_query: null, options: [], } @@ -63,7 +72,10 @@ export default class VeloClientSearch extends Component { } setQuery = (query) => { - this.setState({query: query}); + if(_.isString(this.state.pending_query)) { + query = this.state.pending_query; + } + this.setState({query: query, pending_query: null}); this.props.setSearch(query); } @@ -84,6 +96,17 @@ export default class VeloClientSearch extends Component { }); } + // Ensure the query string is a string. + getQuery = ()=>{ + if(_.isString(this.state.pending_query)) { + return this.state.pending_query; + } + if(_.isString(this.state.query)) { + return this.state.query; + } + return ""; + }; + render() { return (
{ @@ -97,20 +120,22 @@ export default class VeloClientSearch extends Component { onSuggestionsFetchRequested={(x) => this.showSuggestions(x.value)} onSuggestionsClearRequested={() => this.setState({options: []})} onSuggestionSelected={(e, x) => { - this.setQuery(x.suggestionValue); + this.setState({pending_query: null}); + this.props.setSearch(x.suggestionValue); }} getSuggestionValue={x=>x} renderSuggestion={(x) =>
{x}
} inputProps={{ placeholder: T("SEARCH_CLIENTS"), spellCheck: "false", - value: this.state.query, + value: this.getQuery(), id: "client-search-bar", onChange: (e, {newValue, method}) => { - this.setState({query: newValue}); + this.setState({pending_query: newValue}); e.preventDefault(); return false; }, + onBlur: ()=>this.setQuery(this.state.pending_query), }} />