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 filters on FE, capability for exclusion filters (and the globbing we're used to) for path-based filters #1067

Merged
merged 10 commits into from
May 26, 2021
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">
Vigasaurus marked this conversation as resolved.
Show resolved Hide resolved
<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