Skip to content

Commit

Permalink
Adds manual filters on FE, capability for exclusion filters (and the …
Browse files Browse the repository at this point in the history
…globbing we're used to) for path-based filters (plausible#1067)

* First pass on manual filter & path regex/negated filters

Still needs:
- Form structure on filter modal
- Edit filter button
- Filter dropdown UI improvement
- Filter modal mount data collection
- Tests
- Potentially negating other filters

* Second pass - mostly everything user-facing is done

Still needs:
- Tests
- Potentially negating other filters
- Potentially some code cleanup

* Bugfix in realtime view, formatting

* Fixes editing UX on list view

* Adds tests for exclusions and wildcards

* Various UI Improvements

- Makes edit buttons full-length & properly sized
- Adds remove filter button in edit menu

* Changelog

* Makes requested changes, adds different version of filter button

* Makes colorings on top bar elements consistent
  • Loading branch information
Vigasaurus authored May 26, 2021
1 parent 1a93542 commit b6eeb40
Show file tree
Hide file tree
Showing 14 changed files with 470 additions and 105 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ All notable changes to this project will be documented in this file.
- CSV export now includes pageviews, bounce rate and visit duration in addition to visitors plausible/analytics#952
- Send stats to multiple dashboards by configuring a comma-separated list of domains plausible/analytics#968
- 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
- Glob (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 to add new and edit existing filters directly plausible/analytics#1067
- Added `CLICKHOUSE_FLUSH_INTERVAL_MS` and `CLICKHOUSE_MAX_BUFFER_SIZE` configuration parameters plausible/analytics#1073

### Fixed
- Fix weekly report time range plausible/analytics#951
Expand Down
8 changes: 8 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,11 @@ iframe[hidden] {
.pagination-link[disabled] {
@apply cursor-default bg-gray-100 dark:bg-gray-300 pointer-events-none;
}

.filter-list-text:hover ~ .filter-list-edit {
display: flex;
}

.filter-list-text:hover ~ .filter-list-remove {
display: none;
}
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
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 (['goal', 'props'].includes(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">
{['goal', 'props'].includes(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 class="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} />
<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
19 changes: 19 additions & 0 deletions assets/js/dashboard/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,22 @@ export function eventName(query) {
}
return 'pageviews'
}

export const formattedFilters = {
'goal': 'Goal',
'props': 'Props',
'source': 'Source',
'utm_medium': 'UTM Medium',
'utm_source': 'UTM Source',
'utm_campaign': 'UTM Campaign',
'referrer': 'Referrer',
'screen': 'Screen size',
'browser': 'Browser',
'browser_version': 'Browser Version',
'os': 'Operating System',
'os_version': 'Operating System Version',
'country': 'Country',
'page': 'Page',
'entry_page': 'Entry Page',
'exit_page': 'Exit Page'
}
2 changes: 1 addition & 1 deletion assets/js/dashboard/realtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Realtime extends React.Component {
<div className="items-center justify-between w-full sm:flex">
<div className="flex items-center w-full mb-2 sm:mb-0">
<SiteSwitcher site={this.props.site} loggedIn={this.props.loggedIn} />
<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
4 changes: 4 additions & 0 deletions assets/js/dashboard/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import PagesModal from './stats/modals/pages'
import EntryPagesModal from './stats/modals/entry-pages'
import ExitPagesModal from './stats/modals/exit-pages'
import CountriesModal from './stats/modals/countries'
import FilterModal from './stats/modals/filter'

import {BrowserRouter, Switch, Route, useLocation} from "react-router-dom";

Expand Down Expand Up @@ -50,6 +51,9 @@ export default function Router({site, loggedIn}) {
<Route path="/:domain/countries">
<CountriesModal site={site} />
</Route>
<Route path={["/:domain/filter/:field", "/:domain/filter"]}>
<FilterModal site={site} />
</Route>
</Switch>
</Route>
</BrowserRouter>
Expand Down
Loading

0 comments on commit b6eeb40

Please sign in to comment.