Skip to content

Commit

Permalink
Merge branch 'master' into happy-tcp
Browse files Browse the repository at this point in the history
  • Loading branch information
aerosol authored Jun 24, 2024
2 parents 6bb9b28 + 5a0718a commit f336d15
Show file tree
Hide file tree
Showing 69 changed files with 1,694 additions and 639 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-private-images-ghcr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:

- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-private-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:

- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-public-images-ghcr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:

- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-public-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:

- name: Build and push
id: docker_build
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
Expand Down
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
erlang 26.2.1
elixir 1.16.0-otp-26
erlang 27.0
elixir 1.17.1-otp-27
nodejs 21.0.0
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file.

### Added
- Icons for browsers plausible/analytics#4239
- Automatic custom property selection in the dashboard Properties report
- Add `does_not_contain` filter support to dashboard

### Removed
- Deprecate `ECTO_IPV6` and `ECTO_CH_IPV6` env vars in CE plausible/analytics#4245
Expand All @@ -13,9 +15,12 @@ All notable changes to this project will be documented in this file.

- Increase hourly request limit for API keys in CE from 600 to 1000000 (practically removing the limit) plausible/analytics#4200
- Make TCP connections try IPv6 first with IPv4 fallback in CE plausible/analytics#4245
- `is` and `is not` filters in dashboard no longer support wildcards. Use contains/does not contain filter instead.

### Fixed

- Fix access to Stats API feature in CE plausible/analytics#4244

## v2.1.1 - 2024-06-06

### Added
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# platform specific, it makes sense to build it in the docker

#### Builder
FROM hexpm/elixir:1.16.0-erlang-26.2.1-alpine-3.18.4 as buildcontainer
FROM hexpm/elixir:1.17.1-erlang-27.0-alpine-3.18.6 as buildcontainer

ARG MIX_ENV=ce

Expand Down Expand Up @@ -54,7 +54,7 @@ COPY rel rel
RUN mix release plausible

# Main Docker Image
FROM alpine:3.18.4
FROM alpine:3.18.6
LABEL maintainer="plausible.io <[email protected]>"

ARG BUILD_METADATA={}
Expand Down
4 changes: 3 additions & 1 deletion assets/js/dashboard/components/combobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ function optionId(index) {

export default function PlausibleCombobox(props) {
const [options, setOptions] = useState([])
const [loading, setLoading] = useState(false)
const [isLoading, setLoading] = useState(false)
const [isOpen, setOpen] = useState(false)
const [input, setInput] = useState('')
const [highlightedIndex, setHighlightedIndex] = useState(0)
const searchRef = useRef(null)
const containerRef = useRef(null)
const listRef = useRef(null)

const loading = isLoading || !!props.forceLoading

const visibleOptions = [...options]
if (props.freeChoice && input.length > 0 && options.every(option => option.value !== input)) {
visibleOptions.push({value: input, label: input, freeChoice: true})
Expand Down
15 changes: 8 additions & 7 deletions assets/js/dashboard/components/filter-operator-selector.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Fragment } from "react";

import { FILTER_OPERATIONS } from "../util/filters";
import { FILTER_OPERATIONS, FILTER_OPERATIONS_DISPLAY_NAMES } from "../util/filters";
import { Menu, Transition } from "@headlessui/react";
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { isFreeChoiceFilter, supportsIsNot } from "../util/filters";
Expand All @@ -9,20 +9,20 @@ import classNames from "classnames";
export default function FilterOperatorSelector(props) {
const filterName = props.forFilter

function renderTypeItem(type, shouldDisplay) {
function renderTypeItem(operation, shouldDisplay) {
return (
shouldDisplay && (
<Menu.Item>
{({ active }) => (
<span
onClick={() => props.onSelect(type)}
onClick={() => props.onSelect(operation)}
className={classNames("cursor-pointer block px-4 py-2 text-sm", {
"bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100": active,
"text-gray-700 dark:text-gray-200": !active
}
)}
>
{type}
{FILTER_OPERATIONS_DISPLAY_NAMES[operation]}
</span>
)}
</Menu.Item>
Expand All @@ -40,8 +40,8 @@ export default function FilterOperatorSelector(props) {
{({ open }) => (
<>
<div className="w-full">
<Menu.Button className="inline-flex justify-between items-center w-full rounded-md border border-gray-300 dark:border-gray-500 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-850 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 dark:focus:ring-offset-gray-900 focus:ring-indigo-500">
{props.selectedType}
<Menu.Button className="inline-flex justify-between items-center w-full rounded-md border border-gray-300 dark:border-gray-500 shadow-sm px-4 py-2 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-850 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 dark:focus:ring-offset-gray-900 focus:ring-indigo-500 text-left">
{FILTER_OPERATIONS_DISPLAY_NAMES[props.selectedType]}
<ChevronDownIcon className="-mr-2 ml-2 h-4 w-4 text-gray-500 dark:text-gray-400" aria-hidden="true" />
</Menu.Button>
</div>
Expand All @@ -58,12 +58,13 @@ export default function FilterOperatorSelector(props) {
>
<Menu.Items
static
className="z-10 origin-top-left absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none"
className="z-10 origin-top-left absolute left-0 mt-2 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div className="py-1">
{renderTypeItem(FILTER_OPERATIONS.is, true)}
{renderTypeItem(FILTER_OPERATIONS.isNot, supportsIsNot(filterName))}
{renderTypeItem(FILTER_OPERATIONS.contains, isFreeChoiceFilter(filterName))}
{renderTypeItem(FILTER_OPERATIONS.does_not_contain, isFreeChoiceFilter(filterName))}
</div>
</Menu.Items>
</Transition>
Expand Down
9 changes: 5 additions & 4 deletions assets/js/dashboard/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
formattedFilters,
EVENT_PROPS_PREFIX,
getPropertyKeyFromFilterKey,
getLabel
getLabel,
FILTER_OPERATIONS_DISPLAY_NAMES
} from "./util/filters"

const WRAPSTATE = { unwrapped: 0, waiting: 1, wrapped: 2 }
Expand Down Expand Up @@ -41,10 +42,10 @@ function filterText(query, [operation, filterKey, clauses]) {
const formattedFilter = formattedFilters[filterKey]

if (formattedFilter) {
return <>{formattedFilter} {operation} {clauses.map((value) => <b key={value}>{getLabel(query.labels, filterKey, value)}</b>).reduce((prev, curr) => [prev, ' or ', curr])} </>
return <>{formattedFilter} {FILTER_OPERATIONS_DISPLAY_NAMES[operation]} {clauses.map((value) => <b key={value}>{getLabel(query.labels, filterKey, value)}</b>).reduce((prev, curr) => [prev, ' or ', curr])} </>
} else if (filterKey.startsWith(EVENT_PROPS_PREFIX)) {
const propKey = getPropertyKeyFromFilterKey(filterKey)
return <>Property <b>{propKey}</b> {operation} {clauses.map((label) => <b key={label}>{label}</b>).reduce((prev, curr) => [prev, ' or ', curr])} </>
return <>Property <b>{propKey}</b> {FILTER_OPERATIONS_DISPLAY_NAMES[operation]} {clauses.map((label) => <b key={label}>{label}</b>).reduce((prev, curr) => [prev, ' or ', curr])} </>
}

throw new Error(`Unknown filter: ${filterKey}`)
Expand Down Expand Up @@ -131,7 +132,7 @@ function Filters(props) {

window.addEventListener('resize', handleResize, false)
document.addEventListener('keyup', handleKeyup)

return () => {
window.removeEventListener('resize', handleResize, false)
document.removeEventListener("keyup", handleKeyup)
Expand Down
15 changes: 8 additions & 7 deletions assets/js/dashboard/stats/behaviours/goal-conversions.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import * as api from "../../api"
import { EVENT_PROPS_PREFIX, getGoalFilter } from "../../util/filters"

export const SPECIAL_GOALS = {
'404': {title: '404 Pages', prop: 'path'},
'Outbound Link: Click': {title: 'Outbound Links', prop: 'url'},
'Cloaked Link: Click': {title: 'Cloaked Links', prop: 'url'},
'File Download': {title: 'File Downloads', prop: 'url'}
'404': { title: '404 Pages', prop: 'path' },
'Outbound Link: Click': { title: 'Outbound Links', prop: 'url' },
'Cloaked Link: Click': { title: 'Cloaked Links', prop: 'url' },
'File Download': { title: 'File Downloads', prop: 'url' },
'WP Search Queries': { title: 'WordPress Search Queries', prop: 'search_query' },
}

function getSpecialGoal(query) {
Expand Down Expand Up @@ -59,8 +60,8 @@ function SpecialPropBreakdown(props) {
getFilterFor={getFilterFor}
keyLabel={prop}
metrics={[
{name: 'visitors', label: 'Visitors', plot: true},
{name: 'events', label: 'Events', hiddenOnMobile: true},
{ name: 'visitors', label: 'Visitors', plot: true },
{ name: 'events', label: 'Events', hiddenOnMobile: true },
CR_METRIC
]}
detailsLink={url.sitePath(site, `/custom-prop-values/${prop}`)}
Expand All @@ -74,7 +75,7 @@ function SpecialPropBreakdown(props) {
}

export default function GoalConversions(props) {
const {site, query, afterFetchData} = props
const { site, query, afterFetchData } = props

const specialGoal = getSpecialGoal(query)
if (specialGoal) {
Expand Down
53 changes: 36 additions & 17 deletions assets/js/dashboard/stats/behaviours/props.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useCallback, useState } from "react"
import ListReport from "../reports/list";
import React, { useCallback, useEffect, useState } from "react"
import ListReport, { MIN_HEIGHT } from "../reports/list";
import Combobox from '../../components/combobox'
import * as api from '../../api'
import * as url from '../../util/url'
import { CR_METRIC, PERCENTAGE_METRIC } from "../reports/metrics";
import * as storage from "../../util/storage";
import { getFiltersByKeyPrefix, EVENT_PROPS_PREFIX, getPropertyKeyFromFilterKey, getGoalFilter, FILTER_OPERATIONS, hasGoalFilter } from "../../util/filters"
import { EVENT_PROPS_PREFIX, getGoalFilter, FILTER_OPERATIONS, hasGoalFilter } from "../../util/filters"
import classNames from "classnames";


export default function Properties(props) {
Expand All @@ -16,7 +17,8 @@ export default function Properties(props) {
return `${goal}__prop_key__${site.domain}`
}

const [propKey, setPropKey] = useState(choosePropKey())
const [propKey, setPropKey] = useState(null)
const [propKeyLoading, setPropKeyLoading] = useState(true)

function singleGoalFilterApplied() {
const goalFilter = getGoalFilter(query)
Expand All @@ -28,15 +30,26 @@ export default function Properties(props) {
}
}

function choosePropKey() {
const propFilters = getFiltersByKeyPrefix(query, EVENT_PROPS_PREFIX)
if (propFilters.length > 0) {
const [_operation, filterKey, _clauses] = propFilters[0]
return getPropertyKeyFromFilterKey(filterKey)
} else {
return getPropKeyFromStorage()
}
}
useEffect(() => {
setPropKeyLoading(true)
setPropKey(null)

fetchPropKeyOptions()("").then((propKeys) => {
const propKeyValues = propKeys.map(entry => entry.value)

if (propKeyValues.length > 0) {
const storedPropKey = getPropKeyFromStorage()

if (propKeyValues.includes(storedPropKey)) {
setPropKey(storedPropKey)
} else {
setPropKey(propKeys[0].value)
}
}

setPropKeyLoading(false)
})
}, [query])

function getPropKeyFromStorage() {
if (singleGoalFilterApplied()) {
Expand Down Expand Up @@ -99,13 +112,19 @@ export default function Properties(props) {
filter: ["is", `${EVENT_PROPS_PREFIX}${propKey}`, [listItem.name]]
})

const comboboxDisabled = !propKeyLoading && !propKey
const comboboxPlaceholder = comboboxDisabled ? 'No custom properties found' : ''
const comboboxValues = propKey ? [{ value: propKey, label: propKey }] : []
const boxClass = 'pl-2 pr-8 py-1 bg-transparent dark:text-gray-300 rounded-md shadow-sm border border-gray-300 dark:border-gray-500'
const boxClass = classNames('pl-2 pr-8 py-1 bg-transparent dark:text-gray-300 rounded-md shadow-sm border border-gray-300 dark:border-gray-500', {
'pointer-events-none': comboboxDisabled
})

const COMBOBOX_HEIGHT = 40

return (
<div className="w-full mt-4">
<div>
<Combobox boxClass={boxClass} fetchOptions={fetchPropKeyOptions()} singleOption={true} values={comboboxValues} onSelect={onPropKeySelect()} placeholder={'Select a property'} />
<div className="w-full mt-4" style={{ minHeight: `${COMBOBOX_HEIGHT + MIN_HEIGHT}px` }}>
<div style={{ minHeight: `${COMBOBOX_HEIGHT}px` }}>
<Combobox boxClass={boxClass} forceLoading={propKeyLoading} fetchOptions={fetchPropKeyOptions()} singleOption={true} values={comboboxValues} onSelect={onPropKeySelect()} placeholder={comboboxPlaceholder} />
</div>
{propKey && renderBreakdown()}
</div>
Expand Down
36 changes: 26 additions & 10 deletions assets/js/dashboard/stats/devices/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warni
// Icons copied from https://github.com/alrra/browser-logos
const BROWSER_ICONS = {
'Chrome': 'chrome.svg',
'Safari': 'safari.svg',
'Safari': 'safari.png',
'Firefox': 'firefox.svg',
'Microsoft Edge': 'edge.svg',
'Vivaldi': 'vivaldi.svg',
Expand All @@ -28,6 +28,17 @@ const BROWSER_ICONS = {
'vivo Browser': 'vivo.png'
}

function browserIconFor(browser) {
const filename = BROWSER_ICONS[browser] || 'fallback.svg'

return (
<img
src={`/images/icon/browser/${filename}`}
className="w-4 h-4 mr-2"
/>
)
}

function Browsers({ query, site, afterFetchData }) {
function fetchData() {
return api.get(url.apiPath(site, '/browsers'), query)
Expand All @@ -41,14 +52,7 @@ function Browsers({ query, site, afterFetchData }) {
}

function renderIcon(listItem) {
const filename = BROWSER_ICONS[listItem.name] || 'fallback.svg'

return (
<img
src={`/images/icon/browser/${filename}`}
className="w-4 h-4 mr-2"
/>
)
return browserIconFor(listItem.name)
}

return (
Expand All @@ -67,6 +71,15 @@ function Browsers({ query, site, afterFetchData }) {
function BrowserVersions({ query, site, afterFetchData }) {
function fetchData() {
return api.get(url.apiPath(site, '/browser-versions'), query)
.then(res => {
return {...res, results: res.results.map((row => {
return {...row, name: `${row.browser} ${row.name}`}
}))}
})
}

function renderIcon(listItem) {
return browserIconFor(listItem.browser)
}

function getFilterFor(listItem) {
Expand All @@ -86,6 +99,7 @@ function BrowserVersions({ query, site, afterFetchData }) {
getFilterFor={getFilterFor}
keyLabel="Browser version"
metrics={maybeWithCR([VISITORS_METRIC, PERCENTAGE_METRIC], query)}
renderIcon={renderIcon}
query={query}
/>
)
Expand Down Expand Up @@ -150,7 +164,9 @@ function ScreenSizes({ query, site, afterFetchData }) {
}

function renderIcon(screenSize) {
return iconFor(screenSize.name)
return (
<span className="mr-1.5">{iconFor(screenSize.name)}</span>
)
}

function getFilterFor(listItem) {
Expand Down
Loading

0 comments on commit f336d15

Please sign in to comment.