diff --git a/.tool-versions b/.tool-versions
index 675cf7009..12b580032 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,3 +1,4 @@
elixir 1.11.3-otp-23
erlang 23.2.1
nodejs 15.3.0
+python 3.9.4
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1435daf2b..9d4d618d4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,12 +9,13 @@ Make sure Docker, Elixir, Erlang and Node.js are all installed on your developme
### Start the environment:
1. Run both `make postgres` and `make clickhouse`.
-2. Run `mix deps.get`. This will download the required Elixir dependencies.
-2. Run `mix ecto.create`. This will create the required databases in both Postgres and Clickhouse.
-3. Run `mix ecto.migrate` to build the database schema.
-4. Run `npm ci --prefix assets` to install the required node dependencies.
-5. Run `mix phx.server` to start the Phoenix server.
-6. The system is now available on `localhost:8000`.
+2. You can then get set up with the following bits in one go with `make install`.
+ 1. Run `mix deps.get`. This will download the required Elixir dependencies.
+ 2. Run `mix ecto.create`. This will create the required databases in both Postgres and Clickhouse.
+ 3. Run `mix ecto.migrate` to build the database schema.
+ 4. Run `npm ci --prefix assets` to install the required node dependencies.
+3. Run `make server` or `mix phx.server` to start the Phoenix server.
+4. The system is now available on `localhost:8000`.
### Creating an account
@@ -29,4 +30,10 @@ Make sure Docker, Elixir, Erlang and Node.js are all installed on your developme
1. Stop and remove the Postgres container with `make postgres-stop`.
2. Stop and remove the Clickhouse container with `make clickhouse-stop`.
-Volumes are preserved. You'll find that the Postgres and Clickhouse state are retained when you bring them up again the next time: no need to re-register and so on.
\ No newline at end of file
+Volumes are preserved. You'll find that the Postgres and Clickhouse state are retained when you bring them up again the next time: no need to re-register and so on.
+
+Note: Since we are deleting the containers, be careful when deleting volumes with `docker volume prune`. You might accidentally delete the database and would have to go through re-registration process.
+
+### Pre-commit hooks
+
+`pre-commit` requires Python to be available locally and covers JavaScript and CSS. Set up with `pip install --user pre-commit` followed by `pre-commit install`. Conversely, if the prompts are far too bothersome, remove with `pre-commit uninstall`.
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 5e7c5c94e..fee3c1096 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,12 @@
+install:
+ mix deps.get
+ mix ecto.create
+ mix ecto.migrate
+ npm ci --prefix assets
+
+server:
+ mix phx.server
+
clickhouse:
docker run --detach -p 8123:8123 --ulimit nofile=262144:262144 --volume=$$PWD/.clickhouse_db_vol:/var/lib/clickhouse --name plausible_clickhouse yandex/clickhouse-server:21.3.2.5
diff --git a/assets/css/app.css b/assets/css/app.css
index 58376b8b2..e3e0d7660 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -247,8 +247,7 @@ blockquote {
}
.stats-item {
- @apply mt-6;
- width: 100%;
+ min-height: 436px;
}
@screen md {
@@ -257,6 +256,12 @@ blockquote {
margin-right: 6px;
width: calc(50% - 6px);
position: relative;
+ min-height: initial;
+ height: 436px;
+ }
+
+ .stats-item__header {
+ height: inherit;
}
}
diff --git a/assets/js/dashboard/fade-in.js b/assets/js/dashboard/fade-in.js
index 57d8539f7..6cbe19052 100644
--- a/assets/js/dashboard/fade-in.js
+++ b/assets/js/dashboard/fade-in.js
@@ -1,8 +1,12 @@
import React from 'react';
-export default function FadeIn({show, children}) {
- const className = show ? "fade-enter-active" : "fade-enter"
-
- return
{children}
+export default function FadeIn({className, show, children}) {
+ return (
+
+ {children}
+
+)
}
diff --git a/assets/js/dashboard/lazy-loader.js b/assets/js/dashboard/lazy-loader.js
index 4a2222fca..3824ef943 100644
--- a/assets/js/dashboard/lazy-loader.js
+++ b/assets/js/dashboard/lazy-loader.js
@@ -1,10 +1,6 @@
import React from 'react';
export default class extends React.Component {
- constructor(props) {
- super(props)
- }
-
componentDidMount() {
this.observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
@@ -24,7 +20,13 @@ export default class extends React.Component {
render() {
return (
- this.element = el} className={this.props.className} style={this.props.style}>{this.props.children}
+ { this.element = el }}
+ className={this.props.className}
+ style={this.props.style}
+ >
+ {this.props.children}
+
);
}
}
diff --git a/assets/js/dashboard/stats/countries.js b/assets/js/dashboard/stats/countries.js
index b5f247686..96b02e525 100644
--- a/assets/js/dashboard/stats/countries.js
+++ b/assets/js/dashboard/stats/countries.js
@@ -148,7 +148,9 @@ class Countries extends React.Component {
render() {
return (
-
+
{ this.state.loading && }
diff --git a/assets/js/dashboard/stats/devices/browsers.js b/assets/js/dashboard/stats/devices/browsers.js
index 11150ea3c..57862dbf7 100644
--- a/assets/js/dashboard/stats/devices/browsers.js
+++ b/assets/js/dashboard/stats/devices/browsers.js
@@ -14,17 +14,17 @@ export default class Browsers extends React.Component {
this.onVisible = this.onVisible.bind(this)
}
- onVisible() {
- this.fetchBrowsers()
- if (this.props.timer) this.props.timer.onTick(this.fetchBrowsers.bind(this))
- }
-
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.setState({loading: true, browsers: null})
this.fetchBrowsers()
}
}
+
+ onVisible() {
+ this.fetchBrowsers()
+ if (this.props.timer) this.props.timer.onTick(this.fetchBrowsers.bind(this))
+ }
fetchBrowsers() {
if (this.props.query.filters.browser) {
@@ -84,9 +84,9 @@ export default class Browsers extends React.Component {
render() {
return (
-
+
{ this.state.loading && }
-
+
{ this.renderList() }
diff --git a/assets/js/dashboard/stats/devices/index.js b/assets/js/dashboard/stats/devices/index.js
index a8d455322..a07bac301 100644
--- a/assets/js/dashboard/stats/devices/index.js
+++ b/assets/js/dashboard/stats/devices/index.js
@@ -8,7 +8,6 @@ import OperatingSystems from './operating-systems'
import FadeIn from '../../fade-in'
import numberFormatter from '../../number-formatter'
import Bar from '../bar'
-import MoreLink from '../more-link'
import * as api from '../../api'
@@ -46,11 +45,6 @@ class ScreenSizes extends React.Component {
this.onVisible = this.onVisible.bind(this)
}
- onVisible() {
- this.fetchScreenSizes()
- if (this.props.timer) this.props.timer.onTick(this.fetchScreenSizes.bind(this))
- }
-
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.setState({loading: true, sizes: null})
@@ -58,11 +52,24 @@ class ScreenSizes extends React.Component {
}
}
+ onVisible() {
+ this.fetchScreenSizes()
+ if (this.props.timer) this.props.timer.onTick(this.fetchScreenSizes.bind(this))
+ }
+
+
fetchScreenSizes() {
- api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/screen-sizes`, this.props.query)
+ api.get(
+ `/api/stats/${encodeURIComponent(this.props.site.domain)}/screen-sizes`,
+ this.props.query
+ )
.then((res) => this.setState({loading: false, sizes: res}))
}
+ label() {
+ return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
+ }
+
renderScreenSize(size) {
const query = new URLSearchParams(window.location.search)
query.set('screen', size.name)
@@ -70,43 +77,59 @@ class ScreenSizes extends React.Component {
return (
-
-
+
+
{iconFor(size.name)} {size.name}
-
{numberFormatter(size.count)} ({size.percentage}%)
+
+ {numberFormatter(size.count)}
+ ({size.percentage}%)
+
)
}
- label() {
- return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
- }
-
renderList() {
if (this.state.sizes && this.state.sizes.length > 0) {
return (
-
+
Screen size
{ this.label() }
{ this.state.sizes && this.state.sizes.map(this.renderScreenSize.bind(this)) }
)
- } else {
- return
No data yet
}
+ return (
+
+ No data yet
+
+ )
}
render() {
return (
-
+
{ this.state.loading && }
-
+
{ this.renderList() }
@@ -117,57 +140,85 @@ class ScreenSizes extends React.Component {
export default class Devices extends React.Component {
constructor(props) {
super(props)
- this.tabKey = 'deviceTab__' + props.site.domain
+ this.tabKey = `deviceTab__${ props.site.domain}`
const storedTab = storage.getItem(this.tabKey)
this.state = {
mode: storedTab || 'size'
}
}
- renderContent() {
- if (this.state.mode === 'size') {
- return
- } else if (this.state.mode === 'browser') {
- return
- } else if (this.state.mode === 'os') {
- return
- }
- }
-
+
setMode(mode) {
return () => {
storage.setItem(this.tabKey, mode)
this.setState({mode})
}
}
+
+ renderContent() {
+ switch (this.state.mode) {
+ case 'browser':
+ return
+ case 'os':
+ return (
+
+ )
+ case 'size':
+ default:
+ return (
+
+ )
+ }
+ }
renderPill(name, mode) {
const isActive = this.state.mode === mode
if (isActive) {
- return {name}
- } else {
- return {name}
+ return (
+
+ {name}
+
+ )
}
+
+ return (
+
+ {name}
+
+ )
}
render() {
return (
-
-
-
+
+
Devices
-
{ this.renderPill('Size', 'size') }
{ this.renderPill('Browser', 'browser') }
{ this.renderPill('OS', 'os') }
-
{ this.renderContent() }
-
)
diff --git a/assets/js/dashboard/stats/devices/operating-systems.js b/assets/js/dashboard/stats/devices/operating-systems.js
index 7e6011bc5..1b8e1abc0 100644
--- a/assets/js/dashboard/stats/devices/operating-systems.js
+++ b/assets/js/dashboard/stats/devices/operating-systems.js
@@ -83,9 +83,9 @@ export default class OperatingSystems extends React.Component {
render() {
return (
-
+
{ this.state.loading && }
-
+
{ this.renderList() }
diff --git a/assets/js/dashboard/stats/more-link.js b/assets/js/dashboard/stats/more-link.js
index 6878693bc..c0f23cf51 100644
--- a/assets/js/dashboard/stats/more-link.js
+++ b/assets/js/dashboard/stats/more-link.js
@@ -4,9 +4,26 @@ import { Link } from 'react-router-dom'
export default function MoreLink({site, list, endpoint}) {
if (list.length > 0) {
return (
-
-
-
+
+
+
+ {/* eslint-disable-next-line max-len */}
+
+
DETAILS
diff --git a/assets/js/dashboard/stats/pages/entry-pages.js b/assets/js/dashboard/stats/pages/entry-pages.js
index 88c69ea6d..cb6323d77 100644
--- a/assets/js/dashboard/stats/pages/entry-pages.js
+++ b/assets/js/dashboard/stats/pages/entry-pages.js
@@ -42,9 +42,9 @@ export default class EntryPages extends React.Component {
-
+
{page.name}
-
+
@@ -57,7 +57,7 @@ export default class EntryPages extends React.Component {
renderList() {
if (this.state.pages && this.state.pages.length > 0) {
return (
-
+ <>
Page url
Unique Entrances
@@ -66,19 +66,24 @@ export default class EntryPages extends React.Component {
{ this.state.pages.map(this.renderPage.bind(this)) }
-
+ >
)
- } else {
- return
No data yet
- }
+ }
+ return (
+
+ No data yet
+
+ )
}
render() {
const { loading } = this.state;
return (
-
+
{ loading && }
-
+
{ this.renderList() }
{!loading && }
diff --git a/assets/js/dashboard/stats/pages/exit-pages.js b/assets/js/dashboard/stats/pages/exit-pages.js
index 397b3142e..5397165b6 100644
--- a/assets/js/dashboard/stats/pages/exit-pages.js
+++ b/assets/js/dashboard/stats/pages/exit-pages.js
@@ -76,9 +76,9 @@ export default class ExitPages extends React.Component {
render() {
const { loading } = this.state;
return (
-
+
{ loading && }
-
+
{ this.renderList() }
{!loading && }
diff --git a/assets/js/dashboard/stats/pages/index.js b/assets/js/dashboard/stats/pages/index.js
index 08a4ab244..fb6b45ccf 100644
--- a/assets/js/dashboard/stats/pages/index.js
+++ b/assets/js/dashboard/stats/pages/index.js
@@ -1,11 +1,9 @@
import React from 'react';
-import { Link } from 'react-router-dom'
import * as storage from '../../storage'
import Visits from './pages'
import EntryPages from './entry-pages'
import ExitPages from './exit-pages'
-import FadeIn from '../../fade-in'
const labelFor = {
'pages': 'Top Pages',
@@ -16,23 +14,13 @@ const labelFor = {
export default class Pages extends React.Component {
constructor(props) {
super(props)
- this.tabKey = 'pageTab__' + props.site.domain
+ this.tabKey = `pageTab__${ props.site.domain}`
const storedTab = storage.getItem(this.tabKey)
this.state = {
mode: storedTab || 'pages'
}
}
- renderContent() {
- if (this.state.mode === 'pages') {
- return
- } else if (this.state.mode === 'entry-pages') {
- return
- } else if (this.state.mode === 'exit-pages') {
- return
- }
- }
-
setMode(mode) {
return () => {
storage.setItem(this.tabKey, mode)
@@ -40,34 +28,63 @@ export default class Pages extends React.Component {
}
}
+ renderContent() {
+ switch(this.state.mode) {
+ case "entry-pages":
+ return
+ case "exit-pages":
+ return
+ case "pages":
+ default:
+ return
+ }
+ }
+
+
renderPill(name, mode) {
const isActive = this.state.mode === mode
if (isActive) {
- return {name}
- } else {
- return {name}
+ return (
+
+ {name}
+
+ )
}
+
+ return (
+
+ {name}
+
+ )
}
render() {
- const filters = this.props.query.filters
return (
-
-
-
+
+
+ {/* Header Container */}
-
{labelFor[this.state.mode] || 'Page Visits'}
-
+
+ {labelFor[this.state.mode] || 'Page Visits'}
+
{ this.renderPill('Top Pages', 'pages') }
{ this.renderPill('Entry Pages', 'entry-pages') }
{ this.renderPill('Exit Pages', 'exit-pages') }
-
+ {/* Main Contents */}
{ this.renderContent() }
-
)
diff --git a/assets/js/dashboard/stats/pages/pages.js b/assets/js/dashboard/stats/pages/pages.js
index 4ad1d7765..493c3c68f 100644
--- a/assets/js/dashboard/stats/pages/pages.js
+++ b/assets/js/dashboard/stats/pages/pages.js
@@ -87,9 +87,9 @@ export default class Visits extends React.Component {
render() {
const { loading } = this.state;
return (
-
+
{ loading && }
-
+
{ this.renderList() }
{!loading && }
diff --git a/assets/js/dashboard/stats/sources/referrer-list.js b/assets/js/dashboard/stats/sources/referrer-list.js
index 071b1967d..c122fef81 100644
--- a/assets/js/dashboard/stats/sources/referrer-list.js
+++ b/assets/js/dashboard/stats/sources/referrer-list.js
@@ -94,20 +94,27 @@ export default class Referrers extends React.Component {
renderList() {
if (this.state.referrers.length > 0) {
return (
-
-
+
+
Referrer
{ this.label() }
-
+
{this.state.referrers.map(this.renderReferrer.bind(this))}
-
+
)
- } else {
- return
No data yet
}
+ return (
+
+ No data yet
+
+ )
}
renderContent() {
@@ -123,13 +130,15 @@ export default class Referrers extends React.Component {
render() {
return (
-
-
+
+
Top Referrers
{ this.state.loading && }
-
- { this.renderContent() }
-
+
+ { this.renderContent() }
+
)
diff --git a/assets/js/dashboard/stats/sources/search-terms.js b/assets/js/dashboard/stats/sources/search-terms.js
index f453126a1..65018f8cd 100644
--- a/assets/js/dashboard/stats/sources/search-terms.js
+++ b/assets/js/dashboard/stats/sources/search-terms.js
@@ -107,9 +107,11 @@ export default class SearchTerms extends React.Component {
render() {
return (
-
+
{ this.state.loading &&
}
-
+
{ this.renderContent() }
diff --git a/assets/js/dashboard/stats/sources/source-list.js b/assets/js/dashboard/stats/sources/source-list.js
index 1bd378d50..40ca9003a 100644
--- a/assets/js/dashboard/stats/sources/source-list.js
+++ b/assets/js/dashboard/stats/sources/source-list.js
@@ -99,7 +99,9 @@ class AllSources extends React.Component {
render() {
return (
-
+
{ this.renderContent() }
)
@@ -166,17 +168,17 @@ class UTMSources extends React.Component {
renderList() {
if (this.state.referrers && this.state.referrers.length > 0) {
return (
-
+
{UTM_TAGS[this.props.tab].label}
{this.label()}
-
+
{this.state.referrers.map(this.renderReferrer.bind(this))}
-
+
)
} else {
return No data yet
@@ -191,7 +193,7 @@ class UTMSources extends React.Component {
{ this.props.renderTabs() }
{ this.state.loading &&
}
-
+
{ this.renderList() }
@@ -200,7 +202,9 @@ class UTMSources extends React.Component {
render() {
return (
-
+
{ this.renderContent() }
)