mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 17:44:43 +03:00
Revert "Adds manual filters on FE, capability for exclusion filters (and the globbing we're used to) for path-based filters (#1067)"
This reverts commit b6eeb40472
.
This commit is contained in:
parent
2be99db54d
commit
c37a2de627
@ -10,10 +10,7 @@ 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
|
||||
- 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
|
||||
- Added `CLICKHOUSE_FLUSH_INTERVAL_MS` and `CLICKHOUSE_MAX_BUFFER_SIZE` configuration parameters
|
||||
|
||||
### Fixed
|
||||
- Fix weekly report time range plausible/analytics#951
|
||||
|
@ -310,11 +310,3 @@ 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;
|
||||
}
|
||||
|
@ -38,6 +38,7 @@
|
||||
.modal__container {
|
||||
background-color: #fff;
|
||||
padding: 1rem 2rem;
|
||||
max-width: 860px;
|
||||
border-radius: 4px;
|
||||
margin: 50px auto;
|
||||
box-sizing: border-box;
|
||||
|
@ -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-300 dark:bg-gray-950" : "hover:bg-gray-200 dark:hover:bg-gray-900"
|
||||
disabledLeft ? "bg-gray-200 dark:bg-gray-900" : ""
|
||||
}`;
|
||||
const rightClasses = `flex items-center px-2 rounded-r dark:text-gray-100 ${
|
||||
disabledRight ? "bg-gray-300 dark:bg-gray-950" : "hover:bg-gray-200 dark:hover:bg-gray-900"
|
||||
disabledRight ? "bg-gray-200 dark:bg-gray-900" : ""
|
||||
}`;
|
||||
return (
|
||||
<div className="flex rounded shadow bg-white mr-4 cursor-pointer dark:bg-gray-800">
|
||||
@ -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 hover:bg-gray-200 dark:hover:bg-gray-900"
|
||||
dark:text-gray-200 h-full"
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
@ -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-200
|
||||
className={`${boldClass } px-4 py-2 md:text-sm leading-tight hover:bg-gray-100
|
||||
dark:hover:bg-gray-900 hover:text-gray-900 dark:hover:text-gray-100 flex items-center justify-between`}
|
||||
>
|
||||
{text}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { withRouter, Link } from 'react-router-dom'
|
||||
import { countFilters, formattedFilters, navigateToQuery, removeQueryParam } from './query'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { countFilters, navigateToQuery, removeQueryParam } from './query'
|
||||
import Datamap from 'datamaps'
|
||||
import Transition from "../transition.js";
|
||||
|
||||
@ -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
|
||||
|
||||
@ -70,7 +70,7 @@ class Filters extends React.Component {
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.setState({ viewport: window.innerWidth || 639 });
|
||||
this.setState({ viewport: window.innerWidth || 639});
|
||||
}
|
||||
|
||||
handleClick(e) {
|
||||
@ -102,9 +102,6 @@ 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>
|
||||
}
|
||||
@ -114,50 +111,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 is <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Source: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "utm_medium") {
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM medium is <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM medium: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "utm_source") {
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM source is <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM source: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "utm_campaign") {
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM campaign is <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">UTM campaign: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "referrer") {
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Referrer is <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Referrer: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "screen") {
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Screen size is <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Screen size: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "browser") {
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Browser is <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Browser: <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 is <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">{browserName}.Version: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "os") {
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Operating System is <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Operating System: <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 is <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">{osName}.Version: <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 is <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: <b>{selectedCountry.properties.name}</b></span>
|
||||
}
|
||||
if (key === "page") {
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Page is{negated ? ' not' : ''} <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Page: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "entry_page") {
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Entry Page is{negated ? ' not' : ''} <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Entry Page: <b>{value}</b></span>
|
||||
}
|
||||
if (key === "exit_page") {
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Exit Page is{negated ? ' not' : ''} <b>{value}</b></span>
|
||||
return <span className="inline-block max-w-2xs md:max-w-xs truncate">Exit Page: <b>{value}</b></span>
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,50 +171,18 @@ 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-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>
|
||||
<b className="ml-1 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="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 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 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>
|
||||
)
|
||||
}
|
||||
@ -233,15 +198,11 @@ class Filters extends React.Component {
|
||||
|
||||
renderDropDownContent() {
|
||||
const { viewport } = this.state;
|
||||
const { history, query, site } = this.props;
|
||||
const { history, query } = 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
|
||||
@ -278,16 +239,11 @@ class Filters extends React.Component {
|
||||
}
|
||||
|
||||
renderFilterList() {
|
||||
const { history, query, site } = this.props;
|
||||
const { viewport } = this.state;
|
||||
const { history, query } = this.props;
|
||||
|
||||
return (
|
||||
<div id="filters" className="flex flex-grow pl-2 flex-wrap">
|
||||
<div id="filters">
|
||||
{(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>
|
||||
);
|
||||
}
|
||||
@ -295,12 +251,16 @@ class Filters extends React.Component {
|
||||
render() {
|
||||
const { wrapped, viewport } = this.state;
|
||||
|
||||
if (this.appliedFilters.length > 0 && (wrapped === 2 || viewport <= 768)) {
|
||||
if (this.appliedFilters.length > 0) {
|
||||
if (wrapped === 2 || viewport <= 768) {
|
||||
return this.renderDropDown();
|
||||
}
|
||||
|
||||
return this.renderFilterList();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(Filters);
|
||||
|
@ -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 site={this.props.site} query={this.props.query} history={this.props.history} />
|
||||
<Filters query={this.props.query} history={this.props.history} />
|
||||
</div>
|
||||
<Datepicker site={this.props.site} query={this.props.query} />
|
||||
</div>
|
||||
|
@ -159,22 +159,3 @@ 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'
|
||||
}
|
||||
|
@ -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 site={this.props.site} query={this.props.query} history={this.props.history} />
|
||||
<Filters query={this.props.query} history={this.props.history} />
|
||||
</div>
|
||||
<Datepicker site={this.props.site} query={this.props.query} />
|
||||
</div>
|
||||
|
@ -7,7 +7,6 @@ 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";
|
||||
|
||||
@ -51,9 +50,6 @@ 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>
|
||||
|
@ -1,137 +0,0 @@
|
||||
import React from "react";
|
||||
import { withRouter, Redirect } from 'react-router-dom'
|
||||
|
||||
import Modal from './modal'
|
||||
import { parseQuery, formattedFilters, navigateToQuery } from '../../query'
|
||||
|
||||
class FilterModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
query: parseQuery(props.location.search, props.site),
|
||||
selectedFilter: "",
|
||||
negated: false,
|
||||
updatedValue: "",
|
||||
filterSaved: false
|
||||
}
|
||||
|
||||
this.editableGoals = Object.keys(this.state.query.filters).filter(filter => !['goal', 'props'].includes(filter))
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({ selectedFilter: this.props.match.params.field })
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { query, selectedFilter } = this.state
|
||||
|
||||
if (prevState.selectedFilter !== selectedFilter) {
|
||||
const negated = query.filters[selectedFilter] && query.filters[selectedFilter][0] == '!' && this.negationSupported(selectedFilter)
|
||||
const updatedValue = negated ? query.filters[selectedFilter].slice(1) : (query.filters[selectedFilter] || "")
|
||||
|
||||
this.setState({ updatedValue, negated })
|
||||
}
|
||||
}
|
||||
|
||||
negationSupported(filter) {
|
||||
return ['page', 'entry_page', 'exit_page'].includes(filter)
|
||||
}
|
||||
|
||||
renderBody() {
|
||||
const { selectedFilter, negated, updatedValue, query } = this.state;
|
||||
|
||||
const finalFilterValue = (this.negationSupported(selectedFilter) && negated ? '!' : '') + updatedValue
|
||||
const finalizedQuery = new URLSearchParams(window.location.search)
|
||||
const validFilter = this.editableGoals.includes(selectedFilter) && updatedValue
|
||||
finalizedQuery.set(selectedFilter, finalFilterValue)
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<h1 className="text-xl font-bold dark:text-gray-100">{query.filters[selectedFilter] ? 'Edit' : 'Add'} Filter</h1>
|
||||
|
||||
<div className="my-4 border-b border-gray-300"></div>
|
||||
<main className="modal__content">
|
||||
<form className="flex flex-col" onSubmit={() => {
|
||||
if (validFilter) {
|
||||
this.setState({ finalizedQuery })
|
||||
}
|
||||
}}>
|
||||
<select
|
||||
value={selectedFilter}
|
||||
className="my-2 block w-full py-2 pl-3 pr-10 text-base border-gray-300 dark:border-gray-700 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-900 dark:text-gray-300"
|
||||
placeholder="Select a Filter"
|
||||
onChange={(e) => this.setState({ selectedFilter: e.target.value })}
|
||||
>
|
||||
<option disabled value="" className="hidden">Select a Filter</option>
|
||||
{this.editableGoals.map(filter => <option key={filter} value={filter}>{formattedFilters[filter]}</option>)}
|
||||
</select>
|
||||
|
||||
{this.negationSupported(selectedFilter) &&
|
||||
<div className="mt-4 flex items-center">
|
||||
<label className="text-gray-700 dark:text-gray-300 text-sm cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className={"text-indigo-600 bg-gray-100 dark:bg-gray-700 mr-2 relative inline-flex flex-shrink-0 h-6 w-8 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none"}
|
||||
checked={negated}
|
||||
name="exclude"
|
||||
onChange={(e) => this.setState({ negated: e.target.checked })}
|
||||
/>
|
||||
Exclude pages matching this filter
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
|
||||
{selectedFilter &&
|
||||
<input
|
||||
type="text"
|
||||
className="mt-4 bg-gray-100 dark:bg-gray-900 outline-none appearance-none border border-transparent rounded w-full p-2 text-gray-700 dark:text-gray-300 leading-normal focus:outline-none focus:bg-white dark:focus:bg-gray-800 focus:border-gray-300 dark:focus:border-gray-500"
|
||||
value={updatedValue}
|
||||
placeholder="Filter value"
|
||||
onChange={(e) => { this.setState({ updatedValue: e.target.value }) }}
|
||||
/>
|
||||
}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!validFilter}
|
||||
className={"button mt-4 w-2/3 mx-auto"}
|
||||
>
|
||||
{query.filters[selectedFilter] ? 'Update' : 'Add'} Filter
|
||||
</button>
|
||||
|
||||
{query.filters[selectedFilter] &&
|
||||
<button
|
||||
className={"button mt-8 px-4 mx-auto flex bg-red-500 dark:bg-red-500 hover:bg-red-600 dark:hover:bg-red-700 items-center"}
|
||||
onClick={() => {
|
||||
const finalizedQuery = new URLSearchParams(window.location.search)
|
||||
finalizedQuery.delete(selectedFilter)
|
||||
this.setState({ finalizedQuery })
|
||||
}}
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
Remove Filter
|
||||
</button>
|
||||
}
|
||||
|
||||
</form>
|
||||
</main>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { finalizedQuery } = this.state
|
||||
|
||||
if (finalizedQuery) {
|
||||
return <Redirect to={{ pathname: `/${encodeURIComponent(this.props.site.domain)}`, search: finalizedQuery.toString() }} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal site={this.props.site} maxWidth="460px">
|
||||
{ this.renderBody()}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(FilterModal)
|
@ -47,7 +47,7 @@ class Modal extends React.Component {
|
||||
<div className="modal is-open" onClick={this.props.onClick}>
|
||||
<div className="modal__overlay">
|
||||
<button className="modal__close"></button>
|
||||
<div ref={this.node} className="modal__container dark:bg-gray-800" style={{maxWidth: this.props.maxWidth || '860px'}}>
|
||||
<div ref={this.node} className="modal__container dark:bg-gray-800">
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
||||
|
@ -14,7 +14,6 @@ module.exports = {
|
||||
extend: {
|
||||
colors: {
|
||||
orange: colors.orange,
|
||||
'gray-950': 'rgb(13, 18, 30)',
|
||||
'gray-850': 'rgb(26, 32, 44)',
|
||||
'gray-825': 'rgb(37, 47, 63)'
|
||||
},
|
||||
@ -31,7 +30,7 @@ module.exports = {
|
||||
'9': 9,
|
||||
},
|
||||
maxWidth: {
|
||||
'2xs': '15rem',
|
||||
'2xs': '16rem',
|
||||
'3xs': '12rem',
|
||||
}
|
||||
},
|
||||
|
@ -257,7 +257,13 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
from(s in referrers, where: s.referrer_source != "")
|
||||
end
|
||||
|
||||
referrers = apply_page_as_entry_page(referrers, site, query)
|
||||
referrers =
|
||||
if query.filters["page"] do
|
||||
page = query.filters["page"]
|
||||
from(s in referrers, where: s.entry_page == ^page)
|
||||
else
|
||||
referrers
|
||||
end
|
||||
|
||||
referrers =
|
||||
if include_details do
|
||||
@ -320,7 +326,13 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
end
|
||||
|
||||
defp apply_page_as_entry_page(db_query, _site, query) do
|
||||
include_path_filter_entry(db_query, query.filters["page"])
|
||||
page = query.filters["page"]
|
||||
|
||||
if is_binary(page) do
|
||||
from(s in db_query, where: s.entry_page == ^page)
|
||||
else
|
||||
db_query
|
||||
end
|
||||
end
|
||||
|
||||
def utm_mediums(site, query, limit \\ 9, page \\ 1, show_noref \\ false) do
|
||||
@ -1144,9 +1156,21 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
sessions_q
|
||||
end
|
||||
|
||||
sessions_q = include_path_filter_entry(sessions_q, query.filters["entry_page"])
|
||||
sessions_q =
|
||||
if query.filters["entry_page"] do
|
||||
entry_page = query.filters["entry_page"]
|
||||
from(s in sessions_q, where: s.entry_page == ^entry_page)
|
||||
else
|
||||
sessions_q
|
||||
end
|
||||
|
||||
sessions_q = include_path_filter_exit(sessions_q, query.filters["exit_page"])
|
||||
sessions_q =
|
||||
if query.filters["exit_page"] do
|
||||
exit_page = query.filters["exit_page"]
|
||||
from(s in sessions_q, where: s.exit_page == ^exit_page)
|
||||
else
|
||||
sessions_q
|
||||
end
|
||||
|
||||
q =
|
||||
from(e in "events",
|
||||
@ -1170,7 +1194,13 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q
|
||||
end
|
||||
|
||||
q = include_path_filter(q, query.filters["page"])
|
||||
q =
|
||||
if query.filters["page"] do
|
||||
page = query.filters["page"]
|
||||
from(e in q, where: e.pathname == ^page)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
if query.filters["props"] do
|
||||
[{key, val}] = query.filters["props"] |> Enum.into([])
|
||||
@ -1320,9 +1350,21 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q
|
||||
end
|
||||
|
||||
q = include_path_filter_entry(q, query.filters["entry_page"])
|
||||
q =
|
||||
if query.filters["entry_page"] do
|
||||
entry_page = query.filters["entry_page"]
|
||||
from(s in q, where: s.entry_page == ^entry_page)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
q = include_path_filter_exit(q, query.filters["exit_page"])
|
||||
q =
|
||||
if query.filters["exit_page"] do
|
||||
exit_page = query.filters["exit_page"]
|
||||
from(s in q, where: s.exit_page == ^exit_page)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
if query.filters["referrer"] do
|
||||
ref = query.filters["referrer"]
|
||||
@ -1422,7 +1464,13 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q
|
||||
end
|
||||
|
||||
q = include_path_filter(q, query.filters["page"])
|
||||
q =
|
||||
if query.filters["page"] do
|
||||
page = query.filters["page"]
|
||||
from(e in q, where: e.pathname == ^page)
|
||||
else
|
||||
q
|
||||
end
|
||||
|
||||
if query.filters["props"] do
|
||||
[{key, val}] = query.filters["props"] |> Enum.into([])
|
||||
@ -1502,9 +1550,12 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
end
|
||||
|
||||
if path do
|
||||
{contains_regex, path_regex} = convert_path_regex(path)
|
||||
if String.match?(path, ~r/\*/) do
|
||||
path_regex =
|
||||
"^#{path}\/?$"
|
||||
|> String.replace(~r/\*\*/, ".*")
|
||||
|> String.replace(~r/(?<!\.)\*/, "[^/]*")
|
||||
|
||||
if contains_regex do
|
||||
from(e in q, where: fragment("match(?, ?)", e.pathname, ^path_regex))
|
||||
else
|
||||
from(e in q, where: e.pathname == ^path)
|
||||
@ -1513,91 +1564,4 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q
|
||||
end
|
||||
end
|
||||
|
||||
defp check_negated_filter(filter) do
|
||||
negated = String.at(filter, 0) == "!"
|
||||
updated_filter = if negated, do: String.slice(filter, 1..-1), else: filter
|
||||
|
||||
{negated, updated_filter}
|
||||
end
|
||||
|
||||
defp convert_path_regex(path) do
|
||||
contains_regex = String.match?(path, ~r/\*/)
|
||||
|
||||
regex =
|
||||
"^#{path}\/?$"
|
||||
|> String.replace(~r/\*\*/, ".*")
|
||||
|> String.replace(~r/(?<!\.)\*/, "[^/]*")
|
||||
|
||||
{contains_regex, regex}
|
||||
end
|
||||
|
||||
defp include_path_filter(db_query, path) do
|
||||
if path do
|
||||
{negated, path} = check_negated_filter(path)
|
||||
{contains_regex, path_regex} = convert_path_regex(path)
|
||||
|
||||
if contains_regex do
|
||||
if negated do
|
||||
from(e in db_query, where: fragment("not(match(?, ?))", e.pathname, ^path_regex))
|
||||
else
|
||||
from(e in db_query, where: fragment("match(?, ?)", e.pathname, ^path_regex))
|
||||
end
|
||||
else
|
||||
if negated do
|
||||
from(e in db_query, where: e.pathname != ^path)
|
||||
else
|
||||
from(e in db_query, where: e.pathname == ^path)
|
||||
end
|
||||
end
|
||||
else
|
||||
db_query
|
||||
end
|
||||
end
|
||||
|
||||
defp include_path_filter_entry(db_query, path) do
|
||||
if path do
|
||||
{negated, path} = check_negated_filter(path)
|
||||
{contains_regex, path_regex} = convert_path_regex(path)
|
||||
|
||||
if contains_regex do
|
||||
if negated do
|
||||
from(e in db_query, where: fragment("not(match(?, ?))", e.entry_page, ^path_regex))
|
||||
else
|
||||
from(e in db_query, where: fragment("match(?, ?)", e.entry_page, ^path_regex))
|
||||
end
|
||||
else
|
||||
if negated do
|
||||
from(e in db_query, where: e.entry_page != ^path)
|
||||
else
|
||||
from(e in db_query, where: e.entry_page == ^path)
|
||||
end
|
||||
end
|
||||
else
|
||||
db_query
|
||||
end
|
||||
end
|
||||
|
||||
defp include_path_filter_exit(db_query, path) do
|
||||
if path do
|
||||
{negated, path} = check_negated_filter(path)
|
||||
{contains_regex, path_regex} = convert_path_regex(path)
|
||||
|
||||
if contains_regex do
|
||||
if negated do
|
||||
from(e in db_query, where: fragment("not(match(?, ?))", e.exit_page, ^path_regex))
|
||||
else
|
||||
from(e in db_query, where: fragment("match(?, ?)", e.exit_page, ^path_regex))
|
||||
end
|
||||
else
|
||||
if negated do
|
||||
from(e in db_query, where: e.exit_page != ^path)
|
||||
else
|
||||
from(e in db_query, where: e.exit_page == ^path)
|
||||
end
|
||||
end
|
||||
else
|
||||
db_query
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -55,100 +55,6 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
]
|
||||
end
|
||||
|
||||
test "filters pages based on wildards", %{conn: conn, site: site} do
|
||||
filters = Jason.encode!(%{page: "/*re*"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2019-01-01&detailed=true&filters=#{
|
||||
filters
|
||||
}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{
|
||||
"time_on_page" => 1,
|
||||
"bounce_rate" => nil,
|
||||
"count" => 2,
|
||||
"pageviews" => 2,
|
||||
"name" => "/register"
|
||||
},
|
||||
%{
|
||||
"time_on_page" => nil,
|
||||
"bounce_rate" => nil,
|
||||
"count" => 1,
|
||||
"pageviews" => 1,
|
||||
"name" => "/irrelevant"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "filters pages based on exclusion", %{conn: conn, site: site} do
|
||||
filters = Jason.encode!(%{page: "!/"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2019-01-01&detailed=true&filters=#{
|
||||
filters
|
||||
}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{
|
||||
"time_on_page" => 1,
|
||||
"bounce_rate" => nil,
|
||||
"count" => 2,
|
||||
"pageviews" => 2,
|
||||
"name" => "/register"
|
||||
},
|
||||
%{
|
||||
"time_on_page" => nil,
|
||||
"bounce_rate" => nil,
|
||||
"count" => 1,
|
||||
"pageviews" => 1,
|
||||
"name" => "/contact"
|
||||
},
|
||||
%{
|
||||
"time_on_page" => nil,
|
||||
"bounce_rate" => nil,
|
||||
"count" => 1,
|
||||
"pageviews" => 1,
|
||||
"name" => "/irrelevant"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "filters pages based on wildard exclusion", %{conn: conn, site: site} do
|
||||
filters = Jason.encode!(%{page: "!/*re*"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/pages?period=day&date=2019-01-01&detailed=true&filters=#{
|
||||
filters
|
||||
}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == [
|
||||
%{
|
||||
"time_on_page" => 82800,
|
||||
"bounce_rate" => 33.0,
|
||||
"count" => 3,
|
||||
"pageviews" => 3,
|
||||
"name" => "/"
|
||||
},
|
||||
%{
|
||||
"time_on_page" => nil,
|
||||
"bounce_rate" => nil,
|
||||
"count" => 1,
|
||||
"pageviews" => 1,
|
||||
"name" => "/contact"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "returns top pages in realtime report", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/api/stats/#{site.domain}/pages?period=realtime")
|
||||
|
||||
@ -191,18 +97,6 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "filters based on exclusion for entry pages", %{conn: conn, site: site} do
|
||||
filters = Jason.encode!(%{entry_page: "!/"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/entry-pages?period=day&date=2019-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/stats/:domain/exit-pages" do
|
||||
@ -215,17 +109,5 @@ defmodule PlausibleWeb.Api.StatsController.PagesTest do
|
||||
%{"count" => 3, "exits" => 3, "name" => "/", "exit_rate" => 100.0}
|
||||
]
|
||||
end
|
||||
|
||||
test "filters based on exclusion for entry pages", %{conn: conn, site: site} do
|
||||
filters = Jason.encode!(%{exit_page: "!/"})
|
||||
|
||||
conn =
|
||||
get(
|
||||
conn,
|
||||
"/api/stats/#{site.domain}/exit-pages?period=day&date=2019-01-01&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user