mirror of
https://github.com/plausible/analytics.git
synced 2024-12-26 11:02:52 +03:00
Merge branch 'plausible:master' into master
This commit is contained in:
commit
d1e92ded4d
@ -81,7 +81,7 @@ export default function SearchSelect(props) {
|
|||||||
return (
|
return (
|
||||||
<div className="mt-1 relative">
|
<div className="mt-1 relative">
|
||||||
<div className="relative rounded-md shadow-sm" {...getToggleButtonProps()} {...getComboboxProps()}>
|
<div className="relative rounded-md shadow-sm" {...getToggleButtonProps()} {...getComboboxProps()}>
|
||||||
<input {...getInputProps({onKeyDown: keydown})} onFocus={selectInputText} placeholder="Enter a filter value" type="text" className={classNames('w-full pr-10 border-gray-300 dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-200 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-900 dark:text-gray-300 block', {'cursor-pointer': inputValue === '' && !isOpen})} />
|
<input {...getInputProps({onKeyDown: keydown})} onFocus={selectInputText} placeholder={props.placeholder} type="text" className={classNames('w-full pr-10 border-gray-300 dark:border-gray-700 hover:border-gray-400 dark:hover:border-gray-200 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-900 dark:text-gray-300 block', {'cursor-pointer': inputValue === '' && !isOpen})} />
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<div className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
{ !loading && <ChevronDown /> }
|
{ !loading && <ChevronDown /> }
|
||||||
{ loading && <Spinner /> }
|
{ loading && <Spinner /> }
|
||||||
@ -89,17 +89,17 @@ export default function SearchSelect(props) {
|
|||||||
</div>
|
</div>
|
||||||
<div {...getMenuProps()}>
|
<div {...getMenuProps()}>
|
||||||
{ isOpen &&
|
{ isOpen &&
|
||||||
<ul className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
<ul className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-900 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||||
{ !loading && items.length == 0 &&
|
{ !loading && items.length == 0 &&
|
||||||
<li className="cursor-default select-none relative py-2 pl-3 pr-9">No results found</li>
|
<li className="text-gray-500 select-none py-2 px-3">No matches found in the current dashboard. Try selecting a different time range or searching for something different</li>
|
||||||
}
|
}
|
||||||
{ loading && items.length == 0 &&
|
{ loading && items.length == 0 &&
|
||||||
<li className="cursor-default select-none relative py-2 pl-3 pr-9">Loading options...</li>
|
<li className="text-gray-500 select-none py-2 px-3">Loading options...</li>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
items.map((item, index) => (
|
items.map((item, index) => (
|
||||||
<li className={classNames("cursor-pointer select-none relative py-2 pl-3 pr-9", {'text-white bg-indigo-600': highlightedIndex === index, 'text-gray-900': highlightedIndex !== index})}
|
<li className={classNames("cursor-pointer select-none relative py-2 pl-3 pr-9", {'text-white bg-indigo-600': highlightedIndex === index, 'text-gray-900 dark:text-gray-100': highlightedIndex !== index})}
|
||||||
key={`${item}${index}`}
|
key={`${item}${index}`}
|
||||||
{...getItemProps({ item, index })}
|
{...getItemProps({ item, index })}
|
||||||
>
|
>
|
||||||
|
@ -126,7 +126,7 @@ class Filters extends React.Component {
|
|||||||
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 is <b>{value}</b></span>
|
||||||
}
|
}
|
||||||
if (key === "referrer") {
|
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 URL is <b>{value}</b></span>
|
||||||
}
|
}
|
||||||
if (key === "screen") {
|
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 is <b>{value}</b></span>
|
||||||
@ -284,7 +284,7 @@ class Filters extends React.Component {
|
|||||||
<div id="filters" className="flex flex-grow pl-2 flex-wrap">
|
<div id="filters" className="flex flex-grow pl-2 flex-wrap">
|
||||||
{(this.appliedFilters.map((filter) => this.renderListFilter(history, filter, query)))}
|
{(this.appliedFilters.map((filter) => this.renderListFilter(history, filter, query)))}
|
||||||
|
|
||||||
<Link to={`/${encodeURIComponent(site.domain)}/filter${window.location.search}`} className={`inline-flex items-center text-sm font-medium px-4 py-2 mr-2 cursor-pointer ml-auto text-gray-500 dark:text-gray-200 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-900 rounded`}>
|
<Link to={`/${encodeURIComponent(site.domain)}/filter${window.location.search}`} className={`inline-flex items-center text-sm font-medium px-4 py-2 mr-2 cursor-pointer ml-auto text-gray-500 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-900 rounded`}>
|
||||||
<svg className="mr-1 w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M5 4a1 1 0 00-2 0v7.268a2 2 0 000 3.464V16a1 1 0 102 0v-1.268a2 2 0 000-3.464V4zM11 4a1 1 0 10-2 0v1.268a2 2 0 000 3.464V16a1 1 0 102 0V8.732a2 2 0 000-3.464V4zM16 3a1 1 0 011 1v7.268a2 2 0 010 3.464V16a1 1 0 11-2 0v-1.268a2 2 0 010-3.464V4a1 1 0 011-1z"></path></svg>
|
<svg className="mr-1 w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M5 4a1 1 0 00-2 0v7.268a2 2 0 000 3.464V16a1 1 0 102 0v-1.268a2 2 0 000-3.464V4zM11 4a1 1 0 10-2 0v1.268a2 2 0 000 3.464V16a1 1 0 102 0V8.732a2 2 0 000-3.464V4zM16 3a1 1 0 011 1v7.268a2 2 0 010 3.464V16a1 1 0 11-2 0v-1.268a2 2 0 010-3.464V4a1 1 0 011-1z"></path></svg>
|
||||||
Filter
|
Filter
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -21,14 +21,42 @@ function getFilterValue(selectedFilter, query) {
|
|||||||
return {filterValue, negated}
|
return {filterValue, negated}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withIndefiniteArticle(word) {
|
||||||
|
if (word.startsWith('UTM')) {
|
||||||
|
return 'a ' + word
|
||||||
|
} else if (['a', 'e', 'i', 'o', 'u'].some((vowel) => word.toLowerCase().startsWith(vowel))) {
|
||||||
|
return 'an ' + word
|
||||||
|
} else {
|
||||||
|
return 'a ' + word
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECONDARY_FILTERS = {
|
||||||
|
'browser': 'browser_version',
|
||||||
|
'os': 'os_version',
|
||||||
|
'source': 'referrer',
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECONDARY_TO_PRIMARY = Object.keys(SECONDARY_FILTERS)
|
||||||
|
.reduce((res, key) => Object.assign(res, {[SECONDARY_FILTERS[key]]: key}), {});
|
||||||
|
|
||||||
|
function getVersionFilter(forFilter) {
|
||||||
|
return SECONDARY_FILTERS[forFilter]
|
||||||
|
}
|
||||||
|
|
||||||
class FilterModal extends React.Component {
|
class FilterModal extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
const query = parseQuery(props.location.search, props.site)
|
const query = parseQuery(props.location.search, props.site)
|
||||||
const selectedFilter = this.props.match.params.field || 'page'
|
let selectedFilter = this.props.match.params.field || 'page'
|
||||||
|
let secondaryFilter;
|
||||||
|
|
||||||
this.state = Object.assign({selectedFilter, query}, getFilterValue(selectedFilter, query))
|
if (Object.values(SECONDARY_FILTERS).includes(selectedFilter)) {
|
||||||
|
selectedFilter = SECONDARY_TO_PRIMARY[selectedFilter]
|
||||||
|
}
|
||||||
|
secondaryFilter = SECONDARY_FILTERS[selectedFilter]
|
||||||
|
|
||||||
|
this.state = Object.assign({selectedFilter, query}, getFilterValue(selectedFilter, query), {secondaryFilterValue: query.filters[secondaryFilter] || ''})
|
||||||
|
|
||||||
this.handleKeydown = this.handleKeydown.bind(this)
|
this.handleKeydown = this.handleKeydown.bind(this)
|
||||||
this.handleSubmit = this.handleSubmit.bind(this)
|
this.handleSubmit = this.handleSubmit.bind(this)
|
||||||
@ -75,31 +103,68 @@ class FilterModal extends React.Component {
|
|||||||
this.setState({filterValue: val})
|
this.setState({filterValue: val})
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSearchSelector(filter) {
|
renderSearchSelector() {
|
||||||
|
const {selectedFilter, filterValue} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchSelect
|
<SearchSelect
|
||||||
key={this.state.selectedFilter}
|
key={selectedFilter}
|
||||||
fetchOptions={this.fetchOptions.bind(this)}
|
fetchOptions={this.fetchOptions.bind(this)}
|
||||||
initialSelectedItem={this.state.filterValue}
|
initialSelectedItem={filterValue}
|
||||||
onInput={this.onInput.bind(this)}
|
onInput={this.onInput.bind(this)}
|
||||||
|
placeholder={`Select ${withIndefiniteArticle(formattedFilters[selectedFilter])}`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectFilterAndCloseModal(filterKey, filterValue) {
|
fetchSecondaryOptions(filterName) {
|
||||||
|
const {query, selectedFilter} = this.state
|
||||||
|
|
||||||
|
return (input) => {
|
||||||
|
const {filterValue} = this.state
|
||||||
|
const updatedQuery = { ...query, filters: { ...query.filters, [selectedFilter]: filterValue, [filterName]: null } }
|
||||||
|
|
||||||
|
return api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/suggestions/${filterName}`, updatedQuery, { q: input.trim() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSecondaryInput(val) {
|
||||||
|
this.setState({secondaryFilterValue: val})
|
||||||
|
}
|
||||||
|
|
||||||
|
renderVersionSelector() {
|
||||||
|
const {selectedFilter, filterValue, secondaryFilterValue} = this.state
|
||||||
|
const secondaryFilter = SECONDARY_FILTERS[selectedFilter]
|
||||||
|
|
||||||
|
if (secondaryFilter) {
|
||||||
|
return (
|
||||||
|
<SearchSelect
|
||||||
|
key={selectedFilter + filterValue + secondaryFilter}
|
||||||
|
fetchOptions={this.fetchSecondaryOptions(secondaryFilter)}
|
||||||
|
initialSelectedItem={secondaryFilterValue}
|
||||||
|
onInput={this.onSecondaryInput.bind(this)}
|
||||||
|
placeholder={`${formattedFilters[secondaryFilter]} (optional)`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFiltersAndCloseModal(filters) {
|
||||||
const queryString = new URLSearchParams(window.location.search)
|
const queryString = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
if (filterValue) {
|
for (const entry of filters) {
|
||||||
queryString.set(filterKey, filterValue)
|
if (entry.value) {
|
||||||
} else {
|
queryString.set(entry.filter, entry.value)
|
||||||
queryString.delete(filterKey)
|
} else {
|
||||||
|
queryString.delete(entry.filter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.history.replace({pathname: `/${encodeURIComponent(this.props.site.domain)}`, search: queryString.toString()})
|
this.props.history.replace({pathname: `/${encodeURIComponent(this.props.site.domain)}`, search: queryString.toString()})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSubmit() {
|
handleSubmit() {
|
||||||
const { selectedFilter, negated, filterValue } = this.state;
|
const { selectedFilter, negated, filterValue, secondaryFilterValue } = this.state;
|
||||||
|
|
||||||
let finalFilterValue = (this.negationSupported(selectedFilter) && negated ? '!' : '') + filterValue.trim()
|
let finalFilterValue = (this.negationSupported(selectedFilter) && negated ? '!' : '') + filterValue.trim()
|
||||||
if (selectedFilter == 'country') {
|
if (selectedFilter == 'country') {
|
||||||
@ -108,7 +173,15 @@ class FilterModal extends React.Component {
|
|||||||
finalFilterValue = selectedCountry.id
|
finalFilterValue = selectedCountry.id
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectFilterAndCloseModal(selectedFilter, finalFilterValue)
|
const filters = [{filter: selectedFilter, value: finalFilterValue}]
|
||||||
|
|
||||||
|
const secondaryFilter = SECONDARY_FILTERS[selectedFilter]
|
||||||
|
|
||||||
|
if (secondaryFilter) {
|
||||||
|
filters.push({filter: secondaryFilter, value: secondaryFilterValue.trim()})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectFiltersAndCloseModal(filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedFilter(e) {
|
updateSelectedFilter(e) {
|
||||||
@ -116,12 +189,12 @@ class FilterModal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderBody() {
|
renderBody() {
|
||||||
const { selectedFilter, negated, filterValue, query } = this.state;
|
const { selectedFilter, negated, filterValue, secondaryFilterValue, query } = this.state;
|
||||||
const editableFilters = Object.keys(this.state.query.filters).filter(filter => !['props'].includes(filter))
|
const editableFilters = Object.keys(this.state.query.filters).filter(filter => !['props'].concat(Object.values(SECONDARY_FILTERS)).includes(filter))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="text-xl font-bold dark:text-gray-100">{query.filters[selectedFilter] ? 'Edit' : 'Add'} Filter</h1>
|
<h1 className="text-xl font-bold dark:text-gray-100">{query.filters[selectedFilter] || query.filters[SECONDARY_FILTERS[selectedFilter]] ? 'Edit' : 'Add'} Filter</h1>
|
||||||
|
|
||||||
<div className="my-4 border-b border-gray-300"></div>
|
<div className="my-4 border-b border-gray-300"></div>
|
||||||
<main className="modal__content">
|
<main className="modal__content">
|
||||||
@ -152,21 +225,22 @@ class FilterModal extends React.Component {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{this.renderSearchSelector()}
|
{this.renderSearchSelector()}
|
||||||
|
{this.renderVersionSelector()}
|
||||||
|
|
||||||
<div className="mt-6 flex items-center justify-start">
|
<div className="mt-6 flex items-center justify-start">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={filterValue.trim().length === 0}
|
disabled={filterValue.trim().length === 0 && secondaryFilterValue.trim().length === 0}
|
||||||
className="button"
|
className="button"
|
||||||
>
|
>
|
||||||
{query.filters[selectedFilter] ? 'Update' : 'Add'} Filter
|
{query.filters[selectedFilter] || query.filters[SECONDARY_FILTERS[selectedFilter]] ? 'Update' : 'Add'} Filter
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{query.filters[selectedFilter] && (
|
{query.filters[selectedFilter] && (
|
||||||
<button
|
<button
|
||||||
className="ml-2 button px-4 flex bg-red-500 dark:bg-red-500 hover:bg-red-600 dark:hover:bg-red-700 items-center"
|
className="ml-2 button px-4 flex bg-red-500 dark:bg-red-500 hover:bg-red-600 dark:hover:bg-red-700 items-center"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.selectFilterAndCloseModal(selectedFilter, null)
|
this.selectFiltersAndCloseModal([{filter: selectedFilter, value: null}, {filter: SECONDARY_FILTERS[selectedFilter], value: null}])
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg className="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>
|
<svg className="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>
|
||||||
|
Loading…
Reference in New Issue
Block a user