Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds manual, editable, auto-suggested filters, and negated&globbed path-based filters #1121

Merged
merged 4 commits into from
Jun 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ All notable changes to this project will be documented in this file.
- To authenticate against a local postgresql via socket authentication, the environment-variables
`DATABASE_SOCKET_DIR` & `DATABASE_NAME` were added.
- Time on Page metric available in detailed Top Pages report plausible/analytics#1007
- Added `CLICKHOUSE_FLUSH_INTERVAL_MS` and `CLICKHOUSE_MAX_BUFFER_SIZE` configuration parameters
- Wildcard based page, entry page and exit page filters plausible/analytics#1067
- Exclusion filters for page, entry page and exit page filters plausible/analytics#1067
- Menu (with auto-complete) to add new and edit existing filters directly plausible/analytics#1089
- Added `CLICKHOUSE_FLUSH_INTERVAL_MS` and `CLICKHOUSE_MAX_BUFFER_SIZE` configuration parameters plausible/analytics#1073
- Ability to invite users to sites with different roles plausible/analytics#1122

### Fixed
Expand Down
1 change: 0 additions & 1 deletion assets/css/modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
.modal__container {
background-color: #fff;
padding: 1rem 2rem;
max-width: 860px;
border-radius: 4px;
margin: 50px auto;
box-sizing: border-box;
Expand Down
97 changes: 97 additions & 0 deletions assets/css/react-select.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
.filter-list-text:hover ~ .filter-list-edit {
display: flex;
}

.filter-list-text:hover ~ .filter-list-remove {
display: none;
}

.filter-select__control:focus-within {
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgb(99, 102, 241) 0px 0px 0px 1px, rgba(0, 0, 0, 0) 0px 0px 0px 0px; /* indigo-500 */
}

.filter-select__value-container {
padding-left: 1rem !important;
}

.filter-select__control {
@apply dark:bg-gray-900 border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 pr-8;
border-radius: 0.375rem !important;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.35rem 1.35rem;
}

.filter-select__control:hover {
@apply cursor-pointer;
border-color: rgb(156, 163, 175) !important; /* gray-400 */
}

.filter-select__indicator-separator {
@apply hidden;
}

.filter-select__dropdown-indicator {
display: none !important;
}

.filter-select__input {
@apply w-full text-base sm:text-sm;
margin: 0 !important;
}

.filter-select__input input {
width: 100% !important;
box-shadow: none !important;
}

.dark .filter-select__input input {
color: rgb(209, 213, 219) !important; /* gray-300 */
}

.filter-select__value-container > :not(.filter-select__single-value) {
@apply w-full;
padding-top: 0 !important;
}

.filter-select__loading-indicator {
color: rgb(79, 70, 229) !important; /* indigo-600 */
}

.filter-select__loading-indicator > span {
height: 1.5em;
width: 1.5em;
}

.filter-select__single-value {
@apply dark:text-gray-400 text-base sm:text-sm;
}

.filter-select__menu-list {
@apply dark:bg-gray-900;
}

.filter-select__option {
@apply dark:text-gray-300;
font-size: 1rem !important;
line-height: 1.5rem !important;
cursor: pointer !important;
}

@media (min-width: 640px) {
.filter-select__option {
font-size: 0.875rem !important;
line-height: 1.25rem !important;
}
}

.filter-select__option--is-focused {
background-color: rgb(99, 102, 241) !important; /* indigo-500 */
color: white !important;
}

.dark .filter-select__option--is-focused {
background-color: rgb(79, 70, 229) !important; /* indigo-600 */
}

3 changes: 2 additions & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import css from "../css/app.css"
import "../css/app.css"
import "../css/react-select.css"
import "flatpickr/dist/flatpickr.min.css"
import "./polyfills/closest"
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
Expand Down
8 changes: 4 additions & 4 deletions assets/js/dashboard/datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,10 @@ class DatePicker extends React.Component {

const leftClasses = `flex items-center px-2 border-r border-gray-300 rounded-l
dark:border-gray-500 dark:text-gray-100 ${
disabledLeft ? "bg-gray-200 dark:bg-gray-900" : ""
disabledLeft ? "bg-gray-300 dark:bg-gray-950" : "hover:bg-gray-200 dark:hover:bg-gray-900"
}`;
const rightClasses = `flex items-center px-2 rounded-r dark:text-gray-100 ${
disabledRight ? "bg-gray-200 dark:bg-gray-900" : ""
disabledRight ? "bg-gray-300 dark:bg-gray-950" : "hover:bg-gray-200 dark:hover:bg-gray-900"
}`;
return (
<div className="flex rounded shadow bg-white mr-4 cursor-pointer dark:bg-gray-800">
Expand Down Expand Up @@ -242,7 +242,7 @@ class DatePicker extends React.Component {
onKeyPress={this.open}
className="flex items-center justify-between rounded bg-white dark:bg-gray-800 shadow px-4
pr-3 py-2 leading-tight cursor-pointer text-sm font-medium text-gray-800
dark:text-gray-200 h-full"
dark:text-gray-200 h-full hover:bg-gray-200 dark:hover:bg-gray-900"
tabIndex="0"
role="button"
aria-haspopup="true"
Expand Down Expand Up @@ -312,7 +312,7 @@ class DatePicker extends React.Component {
to={{from: false, to: false, period, ...opts}}
onClick={this.close.bind(this)}
query={this.props.query}
className={`${boldClass } px-4 py-2 md:text-sm leading-tight hover:bg-gray-100
className={`${boldClass } px-4 py-2 md:text-sm leading-tight hover:bg-gray-200
dark:hover:bg-gray-900 hover:text-gray-900 dark:hover:text-gray-100 flex items-center justify-between`}
>
{text}
Expand Down
108 changes: 74 additions & 34 deletions assets/js/dashboard/filters.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { withRouter } from 'react-router-dom'
import { countFilters, navigateToQuery, removeQueryParam } from './query'
import { withRouter, Link } from 'react-router-dom'
import { countFilters, formattedFilters, navigateToQuery, removeQueryParam } from './query'
import Datamap from 'datamaps'
import Transition from "../transition.js";

Expand Down Expand Up @@ -60,7 +60,7 @@ class Filters extends React.Component {
}

handleKeyup(e) {
const {query, history} = this.props
const { query, history } = this.props

if (e.ctrlKey || e.metaKey || e.altKey) return

Expand All @@ -70,7 +70,7 @@ class Filters extends React.Component {
}

handleResize() {
this.setState({ viewport: window.innerWidth || 639});
this.setState({ viewport: window.innerWidth || 639 });
}

handleClick(e) {
Expand Down Expand Up @@ -102,6 +102,9 @@ class Filters extends React.Component {
};

filterText(key, value, query) {
const negated = value[0] == '!' && ['page', 'entry_page', 'exit_page'].includes(key)
value = negated ? value.slice(1) : value

if (key === "goal") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Completed goal <b>{value}</b></span>
}
Expand All @@ -111,50 +114,50 @@ class Filters extends React.Component {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">{eventName}.{metaKey} is <b>{metaValue}</b></span>
}
if (key === "source") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Source: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Source is <b>{value}</b></span>
}
if (key === "utm_medium") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM medium: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM medium is <b>{value}</b></span>
}
if (key === "utm_source") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM source: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM source is <b>{value}</b></span>
}
if (key === "utm_campaign") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM campaign: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM campaign is <b>{value}</b></span>
}
if (key === "referrer") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Referrer: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Referrer is <b>{value}</b></span>
}
if (key === "screen") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Screen size: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Screen size is <b>{value}</b></span>
}
if (key === "browser") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Browser: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Browser is <b>{value}</b></span>
}
if (key === "browser_version") {
const browserName = query.filters["browser"] ? query.filters["browser"] : 'Browser'
return <span className="inline-block max-w-2xs md:max-w-xs truncate">{browserName}.Version: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">{browserName} Version is <b>{value}</b></span>
}
if (key === "os") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Operating System: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Operating System is <b>{value}</b></span>
}
if (key === "os_version") {
const osName = query.filters["os"] ? query.filters["os"] : 'OS'
return <span className="inline-block max-w-2xs md:max-w-xs truncate">{osName}.Version: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">{osName} Version is <b>{value}</b></span>
}
if (key === "country") {
const allCountries = Datamap.prototype.worldTopo.objects.world.geometries;
const selectedCountry = allCountries.find((c) => c.id === value) || {properties: {name: value}};
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Country: <b>{selectedCountry.properties.name}</b></span>
const selectedCountry = allCountries.find((c) => c.id === value) || { properties: { name: value } };
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Country is <b>{selectedCountry.properties.name}</b></span>
}
if (key === "page") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Page: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Page is{negated ? ' not' : ''} <b>{value}</b></span>
}
if (key === "entry_page") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Entry Page: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Entry Page is{negated ? ' not' : ''} <b>{value}</b></span>
}
if (key === "exit_page") {
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Exit Page: <b>{value}</b></span>
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Exit Page is{negated ? ' not' : ''} <b>{value}</b></span>
}
}

Expand All @@ -171,18 +174,50 @@ class Filters extends React.Component {
}

renderDropdownFilter(history, [key, value], query) {
if ('props' == key) {
return (
<div className="px-4 sm:py-2 py-3 md:text-sm leading-tight flex items-center justify-between" key={key + value}>
{this.filterText(key, value, query)}
<b title={`Remove filter: ${formattedFilters[key]}`} className="ml-2 cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500" onClick={() => this.removeFilter(key, history, query)}>✕</b>
</div>
)
}

return (
<div className="px-4 sm:py-2 py-3 md:text-sm leading-tight flex items-center justify-between" key={key + value}>
{this.filterText(key, value, query)}
<b className="ml-1 cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500" onClick={() => this.removeFilter(key, history, query)}>✕</b>
<div className="px-3 md:px-4 sm:py-2 py-3 md:text-sm leading-tight flex items-center justify-between" key={key + value}>
<Link
title={`Edit filter: ${formattedFilters[key]}`}
to={{ pathname: `/${encodeURIComponent(this.props.site.domain)}/filter/${key}`, search: window.location.search }}
className="group flex w-full justify-between items-center"
>
{this.filterText(key, value, query)}
<svg className="ml-1 cursor-pointer group-hover:text-indigo-700 dark:group-hover:text-indigo-500 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</Link>
<b title={`Remove filter: ${formattedFilters[key]}`} className="ml-2 cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500" onClick={() => this.removeFilter(key, history, query)}>✕</b>
</div>
)
}

renderListFilter(history, [key, value], query) {
return (
<span key={key} title={value} className="inline-flex bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 shadow text-sm rounded py-2 px-3 mr-2">
{this.filterText(key, value, query)} <b className="ml-1 cursor-pointer hover:text-indigo-500" onClick={() => this.removeFilter(key, history, query)}>✕</b>
<span key={key} title={value} className="flex bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 shadow text-sm rounded mr-2 items-center">
{'props' == key ? (
<span className="flex w-full h-full items-center py-2 pl-3">
{this.filterText(key, value, query)}
</span>
) : (
<>
<Link title={`Edit filter: ${formattedFilters[key]}`} className="filter-list-text flex w-full h-full items-center py-2 pl-3" to={{ pathname: `/${encodeURIComponent(this.props.site.domain)}/filter/${key}`, search: window.location.search }}>
{this.filterText(key, value, query)}
</Link>
<span className="filter-list-edit hidden h-full w-full px-2 cursor-pointer text-indigo-700 dark:text-indigo-500 items-center">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="1 1 23 23" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</span>
</>
)}
<span title={`Remove filter: ${formattedFilters[key]}`} className="filter-list-remove flex h-full w-full px-2 cursor-pointer hover:text-indigo-700 dark:hover:text-indigo-500 items-center" onClick={() => this.removeFilter(key, history, query)}>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</span>
</span>
)
}
Expand All @@ -198,11 +233,15 @@ class Filters extends React.Component {

renderDropDownContent() {
const { viewport } = this.state;
const { history, query } = this.props;
const { history, query, site } = this.props;

return (
<div className="absolute mt-2 rounded shadow-md z-10" style={{ width: viewport <= 768 ? '320px' : '350px', right: '-5px' }} ref={node => this.dropDownNode = node}>
<div className="rounded bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 font-medium text-gray-800 dark:text-gray-200 flex flex-col">
<Link to={`/${encodeURIComponent(site.domain)}/filter${window.location.search}`} className="group border-b flex border-gray-200 dark:border-gray-500 px-4 sm:py-2 py-3 md:text-sm leading-tight hover:text-indigo-700 dark:hover:text-indigo-500 hover:cursor-pointer">
<svg className="mr-2 h-4 w-4 text-gray-500 dark:text-gray-200 group-hover:text-indigo-700 dark:group-hover:text-indigo-500 hover:cursor-pointer" fill="none" stroke="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
Add Filter
</Link>
{this.appliedFilters.map((filter) => this.renderDropdownFilter(history, filter, query))}
<div className="border-t border-gray-200 dark:border-gray-500 px-4 sm:py-2 py-3 md:text-sm leading-tight hover:text-indigo-700 dark:hover:text-indigo-500 hover:cursor-pointer" onClick={() => this.clearAllFilters(history, query)}>
Clear All Filters
Expand Down Expand Up @@ -239,27 +278,28 @@ class Filters extends React.Component {
}

renderFilterList() {
const { history, query } = this.props;
const { history, query, site } = this.props;
const { viewport } = this.state;

return (
<div id="filters">
<div id="filters" className="flex flex-grow pl-2 flex-wrap">
{(this.appliedFilters.map((filter) => this.renderListFilter(history, filter, query)))}
<Link to={`/${encodeURIComponent(site.domain)}/filter${window.location.search}`} className={`button ${viewport <= 768 ? "px-2 mr-1" : "px-3 mr-2"} py-2 cursor-pointer ml-auto text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-900 shadow`}>
<svg className={`${viewport <= 768 ? "mr-1" : "mr-2"} h-4 w-4 text-indigo-500`} fill="none" stroke="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
{viewport <= 768 ? "Filter" : "Add Filter"}
</Link>
</div>
);
}

render() {
const { wrapped, viewport } = this.state;

if (this.appliedFilters.length > 0) {
if (wrapped === 2 || viewport <= 768) {
return this.renderDropDown();
}

return this.renderFilterList();
if (this.appliedFilters.length > 0 && (wrapped === 2 || viewport <= 768)) {
return this.renderDropDown();
}

return null;
return this.renderFilterList();
}
}

Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/historical.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Historical extends React.Component {
<div className="flex items-center w-full mb-2 sm:mb-0">
<SiteSwitcher site={this.props.site} loggedIn={this.props.loggedIn} currentUserRole={this.props.currentUserRole} />
<CurrentVisitors timer={this.props.timer} site={this.props.site} query={this.props.query} />
<Filters query={this.props.query} history={this.props.history} />
<Filters site={this.props.site} query={this.props.query} history={this.props.history} />
</div>
<Datepicker site={this.props.site} query={this.props.query} />
</div>
Expand Down
Loading