mirror of
https://github.com/plausible/analytics.git
synced 2024-11-26 23:27:54 +03:00
Install react-query (#4361)
* WIP * Fix issue with more than one page cached * Make sure we don't access properties of undefined objects * slight cleanup * reset limit to 100 * add back the bottom loading spinner when fetching next page * disable refetchOnWindowFocus in the global queryClient * render bottom loading spinner only when fetching next page * create a wrapper for react-query * fix exhaustive deps warnings in modals * always pass the full api path to BreakdownModal * improve function doc --------- Co-authored-by: Robert Joonas <robertjoonas16@gmail.com> Co-authored-by: RobertJoonas <56999674+RobertJoonas@users.noreply.github.com>
This commit is contained in:
parent
0310cecef8
commit
1acbbf292f
82
assets/js/dashboard/hooks/api-client.js
Normal file
82
assets/js/dashboard/hooks/api-client.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import { useEffect } from "react"
|
||||||
|
import { useQueryClient, useInfiniteQuery } from "@tanstack/react-query"
|
||||||
|
import * as api from "../api"
|
||||||
|
|
||||||
|
const LIMIT = 100
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper for the React Query library. Constructs the necessary options
|
||||||
|
* (including pagination config) to pass into the `useInfiniteQuery` hook.
|
||||||
|
*
|
||||||
|
* ### Required props
|
||||||
|
*
|
||||||
|
* @param {Array} key - The key under which the global "query" instance will live.
|
||||||
|
* Should be passed as a list of two elements - `[endpoint, { query }]`. The object
|
||||||
|
* can also contain additional values (such as `search`) to be used by:
|
||||||
|
* 1) React Query, to determine the uniqueness of the query instance
|
||||||
|
* 2) the `getRequestParams` function to build the request params.
|
||||||
|
*
|
||||||
|
* @param {Function} getRequestParams - A function that takes the `key` prop as an
|
||||||
|
* argument, and returns `[query, params]` which will be used by `queryFn` that
|
||||||
|
* actually calls the API.
|
||||||
|
*
|
||||||
|
* ### Optional props
|
||||||
|
*
|
||||||
|
* @param {Function} [afterFetchData] - A function to call after data has been fetched.
|
||||||
|
* Receives the API response as an argument.
|
||||||
|
*
|
||||||
|
* @param {Function} [afterFetchNextPage] - A function to call after the next page has
|
||||||
|
* been fetched. Receives the API response as an argument.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function useAPIClient(props) {
|
||||||
|
const {key, getRequestParams, afterFetchData, afterFetchNextPage} = props
|
||||||
|
const [endpoint] = key
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const queryFn = async ({ pageParam, queryKey }) => {
|
||||||
|
const [query, params] = getRequestParams(queryKey)
|
||||||
|
params.limit = LIMIT
|
||||||
|
params.page = pageParam
|
||||||
|
|
||||||
|
const response = await api.get(endpoint, query, params)
|
||||||
|
|
||||||
|
if (pageParam === 1 && typeof afterFetchData === 'function') {
|
||||||
|
afterFetchData(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageParam > 1 && typeof afterFetchNextPage === 'function') {
|
||||||
|
afterFetchNextPage(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.results
|
||||||
|
}
|
||||||
|
|
||||||
|
// During the cleanup phase, make sure only the first page of results
|
||||||
|
// is cached under any `queryKey` containing this endpoint.
|
||||||
|
useEffect(() => {
|
||||||
|
const key = [endpoint]
|
||||||
|
return () => {
|
||||||
|
queryClient.setQueriesData(key, (data) => {
|
||||||
|
if (data?.pages?.length) {
|
||||||
|
return {
|
||||||
|
pages: data.pages.slice(0, 1),
|
||||||
|
pageParams: data.pageParams.slice(0, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [queryClient, endpoint])
|
||||||
|
|
||||||
|
const getNextPageParam = (lastPageResults, _, lastPageIndex) => {
|
||||||
|
return lastPageResults.length === LIMIT ? lastPageIndex + 1 : null
|
||||||
|
}
|
||||||
|
const initialPageParam = 1
|
||||||
|
|
||||||
|
return useInfiniteQuery({
|
||||||
|
queryKey: key,
|
||||||
|
queryFn,
|
||||||
|
getNextPageParam,
|
||||||
|
initialPageParam
|
||||||
|
})
|
||||||
|
}
|
@ -15,6 +15,19 @@ import FilterModal from './stats/modals/filter-modal'
|
|||||||
import QueryContextProvider from './query-context';
|
import QueryContextProvider from './query-context';
|
||||||
import { useSiteContext } from './site-context';
|
import { useSiteContext } from './site-context';
|
||||||
|
|
||||||
|
import {
|
||||||
|
QueryClient,
|
||||||
|
QueryClientProvider,
|
||||||
|
} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
refetchOnWindowFocus: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function ScrollToTop() {
|
function ScrollToTop() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@ -30,6 +43,7 @@ function ScrollToTop() {
|
|||||||
export default function Router() {
|
export default function Router() {
|
||||||
const site = useSiteContext()
|
const site = useSiteContext()
|
||||||
return (
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter basename={site.shared ? `/share/${encodeURIComponent(site.domain)}` : encodeURIComponent(site.domain)}>
|
<BrowserRouter basename={site.shared ? `/share/${encodeURIComponent(site.domain)}` : encodeURIComponent(site.domain)}>
|
||||||
<QueryContextProvider>
|
<QueryContextProvider>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
@ -70,5 +84,6 @@ export default function Router() {
|
|||||||
</Route>
|
</Route>
|
||||||
</QueryContextProvider>
|
</QueryContextProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import * as api from '../../api'
|
|
||||||
import { useMountedEffect, useDebounce } from '../../custom-hooks'
|
|
||||||
import { trimURL } from '../../util/url'
|
import { trimURL } from '../../util/url'
|
||||||
import { FilterLink } from "../reports/list";
|
import { FilterLink } from "../reports/list";
|
||||||
import { useQueryContext } from "../../query-context";
|
import { useQueryContext } from "../../query-context";
|
||||||
import { useSiteContext } from "../../site-context";
|
import { useSiteContext } from "../../site-context";
|
||||||
|
import { useDebounce } from "../../custom-hooks";
|
||||||
const LIMIT = 100
|
import { useAPIClient } from "../../hooks/api-client";
|
||||||
const MIN_HEIGHT_PX = 500
|
const MIN_HEIGHT_PX = 500
|
||||||
|
|
||||||
// The main function component for rendering the "Details" reports on the dashboard,
|
// The main function component for rendering the "Details" reports on the dashboard,
|
||||||
@ -39,7 +37,8 @@ const MIN_HEIGHT_PX = 500
|
|||||||
|
|
||||||
// * `title` - the title of the report to render on the top left
|
// * `title` - the title of the report to render on the top left
|
||||||
|
|
||||||
// * `endpoint` - the last part of the endpoint (e.g. "/sources") to query
|
// * `endpoint` - the full pathname of the API endpoint to query. E.g.
|
||||||
|
// `api/stats/plausible.io/sources`
|
||||||
|
|
||||||
// * `dimensionLabel` - a string to render as the dimension column header.
|
// * `dimensionLabel` - a string to render as the dimension column header.
|
||||||
|
|
||||||
@ -84,24 +83,36 @@ export default function BreakdownModal({
|
|||||||
addSearchFilter,
|
addSearchFilter,
|
||||||
getFilterInfo
|
getFilterInfo
|
||||||
}) {
|
}) {
|
||||||
|
const searchBoxRef = useRef(null)
|
||||||
const { query } = useQueryContext();
|
const { query } = useQueryContext();
|
||||||
const site = useSiteContext();
|
const site = useSiteContext();
|
||||||
|
|
||||||
const endpoint = `/api/stats/${encodeURIComponent(site.domain)}${reportInfo.endpoint}`
|
|
||||||
|
|
||||||
const [initialLoading, setInitialLoading] = useState(true)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [results, setResults] = useState([])
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [moreResultsAvailable, setMoreResultsAvailable] = useState(false)
|
|
||||||
const searchBoxRef = useRef(null)
|
|
||||||
|
|
||||||
useEffect(() => { fetchData() }, [])
|
const {
|
||||||
|
data,
|
||||||
|
hasNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
isFetching,
|
||||||
|
isPending
|
||||||
|
} = useAPIClient({
|
||||||
|
key: [reportInfo.endpoint, {query, search}],
|
||||||
|
getRequestParams: (key) => {
|
||||||
|
const [_endpoint, {query, search}] = key
|
||||||
|
|
||||||
useMountedEffect(() => { debouncedFetchData() }, [search])
|
let queryWithSearchFilter = {...query}
|
||||||
|
|
||||||
|
if (searchEnabled && search !== '') {
|
||||||
|
queryWithSearchFilter = addSearchFilter(query, search)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [queryWithSearchFilter, {detailed: true}]
|
||||||
|
},
|
||||||
|
afterFetchData,
|
||||||
|
afterFetchNextPage
|
||||||
|
})
|
||||||
|
|
||||||
useMountedEffect(() => { fetchNextPage() }, [page])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchEnabled) { return }
|
if (!searchEnabled) { return }
|
||||||
@ -120,54 +131,7 @@ export default function BreakdownModal({
|
|||||||
return () => {
|
return () => {
|
||||||
searchBox.removeEventListener('keyup', handleKeyUp);
|
searchBox.removeEventListener('keyup', handleKeyUp);
|
||||||
}
|
}
|
||||||
}, [])
|
}, [searchEnabled])
|
||||||
|
|
||||||
const fetchData = useCallback(() => {
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
api.get(endpoint, withSearch(query), { limit: LIMIT, page: 1, detailed: true })
|
|
||||||
.then((response) => {
|
|
||||||
if (typeof afterFetchData === 'function') {
|
|
||||||
afterFetchData(response)
|
|
||||||
}
|
|
||||||
setInitialLoading(false)
|
|
||||||
setLoading(false)
|
|
||||||
setPage(1)
|
|
||||||
setResults(response.results)
|
|
||||||
setMoreResultsAvailable(response.results.length === LIMIT)
|
|
||||||
})
|
|
||||||
}, [search])
|
|
||||||
|
|
||||||
const debouncedFetchData = useDebounce(fetchData)
|
|
||||||
|
|
||||||
function fetchNextPage() {
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
if (page > 1) {
|
|
||||||
api.get(endpoint, withSearch(query), { limit: LIMIT, page, detailed: true })
|
|
||||||
.then((response) => {
|
|
||||||
if (typeof afterFetchNextPage === 'function') {
|
|
||||||
afterFetchNextPage(response)
|
|
||||||
}
|
|
||||||
setLoading(false)
|
|
||||||
setResults(results.concat(response.results))
|
|
||||||
setMoreResultsAvailable(response.results.length === LIMIT)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function withSearch(query) {
|
|
||||||
if (searchEnabled && search !== '') {
|
|
||||||
return addSearchFilter(query, search)
|
|
||||||
}
|
|
||||||
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadNextPage() {
|
|
||||||
setLoading(true)
|
|
||||||
setPage(page + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeRenderIcon(item) {
|
function maybeRenderIcon(item) {
|
||||||
if (typeof renderIcon === 'function') {
|
if (typeof renderIcon === 'function') {
|
||||||
@ -228,11 +192,13 @@ export default function BreakdownModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderLoadMoreButton() {
|
function renderLoadMoreButton() {
|
||||||
|
if (isPending) return null
|
||||||
|
if (!isFetching && !hasNextPage) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full text-center my-4">
|
<div className="flex flex-col w-full my-4 items-center justify-center h-10">
|
||||||
<button onClick={loadNextPage} type="button" className="button">
|
{!isFetching && <button onClick={fetchNextPage} type="button" className="button">Load more</button>}
|
||||||
Load more
|
{isFetchingNextPage && renderSmallLoadingSpinner()}
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -241,6 +207,8 @@ export default function BreakdownModal({
|
|||||||
setSearch(e.target.value)
|
setSearch(e.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const debouncedHandleInputChange = useDebounce(handleInputChange)
|
||||||
|
|
||||||
function renderSearchInput() {
|
function renderSearchInput() {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
@ -248,13 +216,13 @@ export default function BreakdownModal({
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
className="shadow-sm dark:bg-gray-900 dark:text-gray-100 focus:ring-indigo-500 focus:border-indigo-500 block sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800 w-48"
|
className="shadow-sm dark:bg-gray-900 dark:text-gray-100 focus:ring-indigo-500 focus:border-indigo-500 block sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800 w-48"
|
||||||
onChange={handleInputChange}
|
onChange={debouncedHandleInputChange}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderModalBody() {
|
function renderModalBody() {
|
||||||
if (results) {
|
if (data?.pages?.length) {
|
||||||
return (
|
return (
|
||||||
<main className="modal__content">
|
<main className="modal__content">
|
||||||
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
|
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
|
||||||
@ -277,7 +245,7 @@ export default function BreakdownModal({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{ results.map(renderRow) }
|
{data.pages.map((p) => p.map(renderRow))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</main>
|
</main>
|
||||||
@ -290,15 +258,15 @@ export default function BreakdownModal({
|
|||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<h1 className="text-xl font-bold dark:text-gray-100">{reportInfo.title}</h1>
|
<h1 className="text-xl font-bold dark:text-gray-100">{reportInfo.title}</h1>
|
||||||
{ !initialLoading && loading && renderSmallLoadingSpinner() }
|
{!isPending && isFetching && renderSmallLoadingSpinner()}
|
||||||
</div>
|
</div>
|
||||||
{searchEnabled && renderSearchInput()}
|
{searchEnabled && renderSearchInput()}
|
||||||
</div>
|
</div>
|
||||||
<div className="my-4 border-b border-gray-300"></div>
|
<div className="my-4 border-b border-gray-300"></div>
|
||||||
<div style={{ minHeight: `${MIN_HEIGHT_PX}px` }}>
|
<div style={{ minHeight: `${MIN_HEIGHT_PX}px` }}>
|
||||||
{ initialLoading && renderInitialLoadingSpinner() }
|
{isPending && renderInitialLoadingSpinner()}
|
||||||
{ !initialLoading && renderModalBody() }
|
{!isPending && renderModalBody()}
|
||||||
{ !loading && moreResultsAvailable && renderLoadMoreButton() }
|
{renderLoadMoreButton()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -3,15 +3,18 @@ import React, { useCallback, useState } from "react";
|
|||||||
import Modal from './modal'
|
import Modal from './modal'
|
||||||
import BreakdownModal from "./breakdown-modal";
|
import BreakdownModal from "./breakdown-modal";
|
||||||
import * as metrics from "../reports/metrics";
|
import * as metrics from "../reports/metrics";
|
||||||
|
import * as url from '../../util/url';
|
||||||
|
import { useSiteContext } from "../../site-context";
|
||||||
|
|
||||||
/*global BUILD_EXTRA*/
|
/*global BUILD_EXTRA*/
|
||||||
function ConversionsModal() {
|
function ConversionsModal() {
|
||||||
const [showRevenue, setShowRevenue] = useState(false)
|
const [showRevenue, setShowRevenue] = useState(false)
|
||||||
|
const site = useSiteContext();
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: 'Goal Conversions',
|
title: 'Goal Conversions',
|
||||||
dimension: 'goal',
|
dimension: 'goal',
|
||||||
endpoint: '/conversions',
|
endpoint: url.apiPath(site, '/conversions'),
|
||||||
dimensionLabel: "Goal"
|
dimensionLabel: "Goal"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,7 +23,7 @@ function ConversionsModal() {
|
|||||||
prefix: reportInfo.dimension,
|
prefix: reportInfo.dimension,
|
||||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||||
}
|
}
|
||||||
}, [])
|
}, [reportInfo.dimension])
|
||||||
|
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
@ -37,7 +40,7 @@ function ConversionsModal() {
|
|||||||
// whether revenue metrics are passed into BreakdownModal in `metrics`.
|
// whether revenue metrics are passed into BreakdownModal in `metrics`.
|
||||||
const afterFetchData = useCallback((res) => {
|
const afterFetchData = useCallback((res) => {
|
||||||
setShowRevenue(revenueInResponse(res))
|
setShowRevenue(revenueInResponse(res))
|
||||||
}, [showRevenue])
|
}, [])
|
||||||
|
|
||||||
// After fetching the next page, we never want to set `showRevenue` to
|
// After fetching the next page, we never want to set `showRevenue` to
|
||||||
// `false` as revenue metrics might exist in previously loaded data.
|
// `false` as revenue metrics might exist in previously loaded data.
|
||||||
|
@ -4,15 +4,18 @@ import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
|||||||
import { addFilter } from '../../query'
|
import { addFilter } from '../../query'
|
||||||
import BreakdownModal from "./breakdown-modal";
|
import BreakdownModal from "./breakdown-modal";
|
||||||
import * as metrics from '../reports/metrics'
|
import * as metrics from '../reports/metrics'
|
||||||
|
import * as url from '../../util/url';
|
||||||
import { useQueryContext } from "../../query-context";
|
import { useQueryContext } from "../../query-context";
|
||||||
|
import { useSiteContext } from "../../site-context";
|
||||||
|
|
||||||
function EntryPagesModal() {
|
function EntryPagesModal() {
|
||||||
const { query } = useQueryContext();
|
const { query } = useQueryContext();
|
||||||
|
const site = useSiteContext();
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: 'Entry Pages',
|
title: 'Entry Pages',
|
||||||
dimension: 'entry_page',
|
dimension: 'entry_page',
|
||||||
endpoint: '/entry-pages',
|
endpoint: url.apiPath(site, '/entry-pages'),
|
||||||
dimensionLabel: 'Entry page'
|
dimensionLabel: 'Entry page'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,11 +24,11 @@ function EntryPagesModal() {
|
|||||||
prefix: reportInfo.dimension,
|
prefix: reportInfo.dimension,
|
||||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||||
}
|
}
|
||||||
}, [])
|
}, [reportInfo.dimension])
|
||||||
|
|
||||||
const addSearchFilter = useCallback((query, searchString) => {
|
const addSearchFilter = useCallback((query, searchString) => {
|
||||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||||
}, [])
|
}, [reportInfo.dimension])
|
||||||
|
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
if (hasGoalFilter(query)) {
|
if (hasGoalFilter(query)) {
|
||||||
|
@ -4,15 +4,18 @@ import { hasGoalFilter } from "../../util/filters";
|
|||||||
import { addFilter } from '../../query'
|
import { addFilter } from '../../query'
|
||||||
import BreakdownModal from "./breakdown-modal";
|
import BreakdownModal from "./breakdown-modal";
|
||||||
import * as metrics from '../reports/metrics'
|
import * as metrics from '../reports/metrics'
|
||||||
|
import * as url from '../../util/url';
|
||||||
import { useQueryContext } from "../../query-context";
|
import { useQueryContext } from "../../query-context";
|
||||||
|
import { useSiteContext } from "../../site-context";
|
||||||
|
|
||||||
function ExitPagesModal() {
|
function ExitPagesModal() {
|
||||||
const { query } = useQueryContext();
|
const { query } = useQueryContext();
|
||||||
|
const site = useSiteContext();
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: 'Exit Pages',
|
title: 'Exit Pages',
|
||||||
dimension: 'exit_page',
|
dimension: 'exit_page',
|
||||||
endpoint: '/exit-pages',
|
endpoint: url.apiPath(site, '/exit-pages'),
|
||||||
dimensionLabel: 'Page url'
|
dimensionLabel: 'Page url'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,11 +24,11 @@ function ExitPagesModal() {
|
|||||||
prefix: reportInfo.dimension,
|
prefix: reportInfo.dimension,
|
||||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||||
}
|
}
|
||||||
}, [])
|
}, [reportInfo.dimension])
|
||||||
|
|
||||||
const addSearchFilter = useCallback((query, searchString) => {
|
const addSearchFilter = useCallback((query, searchString) => {
|
||||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||||
}, [])
|
}, [reportInfo.dimension])
|
||||||
|
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
if (hasGoalFilter(query)) {
|
if (hasGoalFilter(query)) {
|
||||||
|
@ -5,7 +5,9 @@ import Modal from './modal'
|
|||||||
import { hasGoalFilter } from "../../util/filters";
|
import { hasGoalFilter } from "../../util/filters";
|
||||||
import BreakdownModal from "./breakdown-modal";
|
import BreakdownModal from "./breakdown-modal";
|
||||||
import * as metrics from "../reports/metrics";
|
import * as metrics from "../reports/metrics";
|
||||||
|
import * as url from '../../util/url';
|
||||||
import { useQueryContext } from "../../query-context";
|
import { useQueryContext } from "../../query-context";
|
||||||
|
import { useSiteContext } from "../../site-context";
|
||||||
|
|
||||||
const VIEWS = {
|
const VIEWS = {
|
||||||
countries: { title: 'Top Countries', dimension: 'country', endpoint: '/countries', dimensionLabel: 'Country' },
|
countries: { title: 'Top Countries', dimension: 'country', endpoint: '/countries', dimensionLabel: 'Country' },
|
||||||
@ -15,18 +17,20 @@ const VIEWS = {
|
|||||||
|
|
||||||
function LocationsModal({ location }) {
|
function LocationsModal({ location }) {
|
||||||
const { query } = useQueryContext();
|
const { query } = useQueryContext();
|
||||||
|
const site = useSiteContext();
|
||||||
|
|
||||||
const urlParts = location.pathname.split('/')
|
const urlParts = location.pathname.split('/')
|
||||||
const currentView = urlParts[urlParts.length - 1]
|
const currentView = urlParts[urlParts.length - 1]
|
||||||
|
|
||||||
const reportInfo = VIEWS[currentView]
|
let reportInfo = VIEWS[currentView]
|
||||||
|
reportInfo.endpoint = url.apiPath(site, reportInfo.endpoint)
|
||||||
|
|
||||||
const getFilterInfo = useCallback((listItem) => {
|
const getFilterInfo = useCallback((listItem) => {
|
||||||
return {
|
return {
|
||||||
prefix: reportInfo.dimension,
|
prefix: reportInfo.dimension,
|
||||||
filter: ["is", reportInfo.dimension, [listItem.code]]
|
filter: ["is", reportInfo.dimension, [listItem.code]]
|
||||||
}
|
}
|
||||||
}, [])
|
}, [reportInfo.dimension])
|
||||||
|
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
if (hasGoalFilter(query)) {
|
if (hasGoalFilter(query)) {
|
||||||
|
@ -4,15 +4,18 @@ import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
|||||||
import { addFilter } from '../../query'
|
import { addFilter } from '../../query'
|
||||||
import BreakdownModal from "./breakdown-modal";
|
import BreakdownModal from "./breakdown-modal";
|
||||||
import * as metrics from '../reports/metrics'
|
import * as metrics from '../reports/metrics'
|
||||||
|
import * as url from '../../util/url';
|
||||||
import { useQueryContext } from "../../query-context";
|
import { useQueryContext } from "../../query-context";
|
||||||
|
import { useSiteContext } from "../../site-context";
|
||||||
|
|
||||||
function PagesModal() {
|
function PagesModal() {
|
||||||
const { query } = useQueryContext();
|
const { query } = useQueryContext();
|
||||||
|
const site = useSiteContext();
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: 'Top Pages',
|
title: 'Top Pages',
|
||||||
dimension: 'page',
|
dimension: 'page',
|
||||||
endpoint: '/pages',
|
endpoint: url.apiPath(site, '/pages'),
|
||||||
dimensionLabel: 'Page url'
|
dimensionLabel: 'Page url'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,11 +24,11 @@ function PagesModal() {
|
|||||||
prefix: reportInfo.dimension,
|
prefix: reportInfo.dimension,
|
||||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||||
}
|
}
|
||||||
}, [])
|
}, [reportInfo.dimension])
|
||||||
|
|
||||||
const addSearchFilter = useCallback((query, searchString) => {
|
const addSearchFilter = useCallback((query, searchString) => {
|
||||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||||
}, [])
|
}, [reportInfo.dimension])
|
||||||
|
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
if (hasGoalFilter(query)) {
|
if (hasGoalFilter(query)) {
|
||||||
|
@ -7,6 +7,7 @@ import { specialTitleWhenGoalFilter } from "../behaviours/goal-conversions";
|
|||||||
import { EVENT_PROPS_PREFIX, hasGoalFilter } from "../../util/filters"
|
import { EVENT_PROPS_PREFIX, hasGoalFilter } from "../../util/filters"
|
||||||
import BreakdownModal from "./breakdown-modal";
|
import BreakdownModal from "./breakdown-modal";
|
||||||
import * as metrics from "../reports/metrics";
|
import * as metrics from "../reports/metrics";
|
||||||
|
import * as url from "../../util/url";
|
||||||
import { revenueAvailable } from "../../query";
|
import { revenueAvailable } from "../../query";
|
||||||
import { useQueryContext } from "../../query-context";
|
import { useQueryContext } from "../../query-context";
|
||||||
import { useSiteContext } from "../../site-context";
|
import { useSiteContext } from "../../site-context";
|
||||||
@ -22,7 +23,7 @@ function PropsModal({ location }) {
|
|||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: specialTitleWhenGoalFilter(query, 'Custom Property Breakdown'),
|
title: specialTitleWhenGoalFilter(query, 'Custom Property Breakdown'),
|
||||||
dimension: propKey,
|
dimension: propKey,
|
||||||
endpoint: `/custom-prop-values/${propKey}`,
|
endpoint: url.apiPath(site, `/custom-prop-values/${propKey}`),
|
||||||
dimensionLabel: propKey
|
dimensionLabel: propKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,11 +32,11 @@ function PropsModal({ location }) {
|
|||||||
prefix: `${EVENT_PROPS_PREFIX}${propKey}`,
|
prefix: `${EVENT_PROPS_PREFIX}${propKey}`,
|
||||||
filter: ["is", `${EVENT_PROPS_PREFIX}${propKey}`, [listItem.name]]
|
filter: ["is", `${EVENT_PROPS_PREFIX}${propKey}`, [listItem.name]]
|
||||||
}
|
}
|
||||||
}, [])
|
}, [propKey])
|
||||||
|
|
||||||
const addSearchFilter = useCallback((query, searchString) => {
|
const addSearchFilter = useCallback((query, searchString) => {
|
||||||
return addFilter(query, ['contains', `${EVENT_PROPS_PREFIX}${propKey}`, [searchString]])
|
return addFilter(query, ['contains', `${EVENT_PROPS_PREFIX}${propKey}`, [searchString]])
|
||||||
}, [])
|
}, [propKey])
|
||||||
|
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
return [
|
return [
|
||||||
|
@ -5,16 +5,19 @@ import Modal from './modal'
|
|||||||
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
||||||
import BreakdownModal from "./breakdown-modal";
|
import BreakdownModal from "./breakdown-modal";
|
||||||
import * as metrics from "../reports/metrics";
|
import * as metrics from "../reports/metrics";
|
||||||
|
import * as url from "../../util/url";
|
||||||
import { addFilter } from "../../query";
|
import { addFilter } from "../../query";
|
||||||
import { useQueryContext } from "../../query-context";
|
import { useQueryContext } from "../../query-context";
|
||||||
|
import { useSiteContext } from "../../site-context";
|
||||||
|
|
||||||
function ReferrerDrilldownModal({ match }) {
|
function ReferrerDrilldownModal({ match }) {
|
||||||
const { query } = useQueryContext();
|
const { query } = useQueryContext();
|
||||||
|
const site = useSiteContext();
|
||||||
|
|
||||||
const reportInfo = {
|
const reportInfo = {
|
||||||
title: "Referrer Drilldown",
|
title: "Referrer Drilldown",
|
||||||
dimension: 'referrer',
|
dimension: 'referrer',
|
||||||
endpoint: `/referrers/${match.params.referrer}`,
|
endpoint: url.apiPath(site, `/referrers/${match.params.referrer}`),
|
||||||
dimensionLabel: "Referrer"
|
dimensionLabel: "Referrer"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,11 +26,11 @@ function ReferrerDrilldownModal({ match }) {
|
|||||||
prefix: reportInfo.dimension,
|
prefix: reportInfo.dimension,
|
||||||
filter: ['is', reportInfo.dimension, [listItem.name]]
|
filter: ['is', reportInfo.dimension, [listItem.name]]
|
||||||
}
|
}
|
||||||
}, [])
|
}, [reportInfo.dimension])
|
||||||
|
|
||||||
const addSearchFilter = useCallback((query, searchString) => {
|
const addSearchFilter = useCallback((query, searchString) => {
|
||||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||||
}, [])
|
}, [reportInfo.dimension])
|
||||||
|
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
if (hasGoalFilter(query)) {
|
if (hasGoalFilter(query)) {
|
||||||
|
@ -5,8 +5,10 @@ import Modal from './modal'
|
|||||||
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
||||||
import BreakdownModal from "./breakdown-modal";
|
import BreakdownModal from "./breakdown-modal";
|
||||||
import * as metrics from "../reports/metrics";
|
import * as metrics from "../reports/metrics";
|
||||||
|
import * as url from "../../util/url";
|
||||||
import { addFilter } from "../../query";
|
import { addFilter } from "../../query";
|
||||||
import { useQueryContext } from "../../query-context";
|
import { useQueryContext } from "../../query-context";
|
||||||
|
import { useSiteContext } from "../../site-context";
|
||||||
|
|
||||||
const VIEWS = {
|
const VIEWS = {
|
||||||
sources: {
|
sources: {
|
||||||
@ -39,22 +41,24 @@ const VIEWS = {
|
|||||||
|
|
||||||
function SourcesModal({ location }) {
|
function SourcesModal({ location }) {
|
||||||
const { query } = useQueryContext();
|
const { query } = useQueryContext();
|
||||||
|
const site = useSiteContext();
|
||||||
|
|
||||||
const urlParts = location.pathname.split('/')
|
const urlParts = location.pathname.split('/')
|
||||||
const currentView = urlParts[urlParts.length - 1]
|
const currentView = urlParts[urlParts.length - 1]
|
||||||
|
|
||||||
const reportInfo = VIEWS[currentView].info
|
let reportInfo = VIEWS[currentView].info
|
||||||
|
reportInfo.endpoint = url.apiPath(site, reportInfo.endpoint)
|
||||||
|
|
||||||
const getFilterInfo = useCallback((listItem) => {
|
const getFilterInfo = useCallback((listItem) => {
|
||||||
return {
|
return {
|
||||||
prefix: reportInfo.dimension,
|
prefix: reportInfo.dimension,
|
||||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||||
}
|
}
|
||||||
}, [])
|
}, [reportInfo.dimension])
|
||||||
|
|
||||||
const addSearchFilter = useCallback((query, searchString) => {
|
const addSearchFilter = useCallback((query, searchString) => {
|
||||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||||
}, [])
|
}, [reportInfo.dimension])
|
||||||
|
|
||||||
function chooseMetrics() {
|
function chooseMetrics() {
|
||||||
if (hasGoalFilter(query)) {
|
if (hasGoalFilter(query)) {
|
||||||
|
25
assets/package-lock.json
generated
25
assets/package-lock.json
generated
@ -17,6 +17,7 @@
|
|||||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
"@tailwindcss/forms": "^0.5.6",
|
"@tailwindcss/forms": "^0.5.6",
|
||||||
"@tailwindcss/typography": "^0.4.1",
|
"@tailwindcss/typography": "^0.4.1",
|
||||||
|
"@tanstack/react-query": "^5.51.1",
|
||||||
"abortcontroller-polyfill": "^1.7.3",
|
"abortcontroller-polyfill": "^1.7.3",
|
||||||
"alpinejs": "^3.13.1",
|
"alpinejs": "^3.13.1",
|
||||||
"chart.js": "^3.3.2",
|
"chart.js": "^3.3.2",
|
||||||
@ -446,6 +447,30 @@
|
|||||||
"tailwindcss": ">=2.0.0"
|
"tailwindcss": ">=2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.51.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.1.tgz",
|
||||||
|
"integrity": "sha512-fJBMQMpo8/KSsWW5ratJR5+IFr7YNJ3K2kfP9l5XObYHsgfVy1w3FJUWU4FT2fj7+JMaEg33zOcNDBo0LMwHnw==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.51.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.1.tgz",
|
||||||
|
"integrity": "sha512-s47HKFnQ4HOJAHoIiXcpna/roMMPZJPy6fJ6p4ZNVn8+/onlLBEDd1+xc8OnDuwgvecqkZD7Z2mnSRbcWefrKw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.51.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/d3": {
|
"node_modules/@types/d3": {
|
||||||
"version": "3.5.38",
|
"version": "3.5.38",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
"@tailwindcss/forms": "^0.5.6",
|
"@tailwindcss/forms": "^0.5.6",
|
||||||
"@tailwindcss/typography": "^0.4.1",
|
"@tailwindcss/typography": "^0.4.1",
|
||||||
|
"@tanstack/react-query": "^5.51.1",
|
||||||
"abortcontroller-polyfill": "^1.7.3",
|
"abortcontroller-polyfill": "^1.7.3",
|
||||||
"alpinejs": "^3.13.1",
|
"alpinejs": "^3.13.1",
|
||||||
"chart.js": "^3.3.2",
|
"chart.js": "^3.3.2",
|
||||||
|
Loading…
Reference in New Issue
Block a user