Merge branch 'plausible:master' into master

This commit is contained in:
Ru Singh 2021-07-05 09:51:26 +05:30 committed by GitHub
commit d1e92ded4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 99 additions and 25 deletions

View File

@ -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 })}
> >

View File

@ -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>

View File

@ -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>