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:
Artur Pata 2024-07-22 11:09:45 +03:00 committed by GitHub
parent 0310cecef8
commit 1acbbf292f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 263 additions and 148 deletions

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

View File

@ -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,45 +43,47 @@ function ScrollToTop() {
export default function Router() { export default function Router() {
const site = useSiteContext() const site = useSiteContext()
return ( return (
<BrowserRouter basename={site.shared ? `/share/${encodeURIComponent(site.domain)}` : encodeURIComponent(site.domain)}> <QueryClientProvider client={queryClient}>
<QueryContextProvider> <BrowserRouter basename={site.shared ? `/share/${encodeURIComponent(site.domain)}` : encodeURIComponent(site.domain)}>
<Route path="/"> <QueryContextProvider>
<ScrollToTop /> <Route path="/">
<Dashboard /> <ScrollToTop />
<Switch> <Dashboard />
<Route exact path={["/sources", "/utm_mediums", "/utm_sources", "/utm_campaigns", "/utm_contents", "/utm_terms"]}> <Switch>
<SourcesModal /> <Route exact path={["/sources", "/utm_mediums", "/utm_sources", "/utm_campaigns", "/utm_contents", "/utm_terms"]}>
</Route> <SourcesModal />
<Route exact path="/referrers/Google"> </Route>
<GoogleKeywordsModal site={site} /> <Route exact path="/referrers/Google">
</Route> <GoogleKeywordsModal site={site} />
<Route exact path="/referrers/:referrer"> </Route>
<ReferrersDrilldownModal /> <Route exact path="/referrers/:referrer">
</Route> <ReferrersDrilldownModal />
<Route path="/pages"> </Route>
<PagesModal /> <Route path="/pages">
</Route> <PagesModal />
<Route path="/entry-pages"> </Route>
<EntryPagesModal /> <Route path="/entry-pages">
</Route> <EntryPagesModal />
<Route path="/exit-pages"> </Route>
<ExitPagesModal /> <Route path="/exit-pages">
</Route> <ExitPagesModal />
<Route exact path={["/countries", "/regions", "/cities"]}> </Route>
<LocationsModal /> <Route exact path={["/countries", "/regions", "/cities"]}>
</Route> <LocationsModal />
<Route path="/custom-prop-values/:prop_key"> </Route>
<PropsModal /> <Route path="/custom-prop-values/:prop_key">
</Route> <PropsModal />
<Route path="/conversions"> </Route>
<ConversionsModal /> <Route path="/conversions">
</Route> <ConversionsModal />
<Route path={["/filter/:field"]}> </Route>
<FilterModal site={site} /> <Route path={["/filter/:field"]}>
</Route> <FilterModal site={site} />
</Switch> </Route>
</Route> </Switch>
</QueryContextProvider> </Route>
</BrowserRouter> </QueryContextProvider>
</BrowserRouter>
</QueryClientProvider>
); );
} }

View File

@ -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 {query} = useQueryContext(); const searchBoxRef = useRef(null)
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') {
@ -179,7 +143,7 @@ export default function BreakdownModal({
if (typeof getExternalLinkURL === 'function') { if (typeof getExternalLinkURL === 'function') {
const linkUrl = getExternalLinkURL(item) const linkUrl = getExternalLinkURL(item)
if (!linkUrl) { return null} if (!linkUrl) { return null }
return ( return (
<a target="_blank" href={linkUrl} rel="noreferrer" className="hidden group-hover:block"> <a target="_blank" href={linkUrl} rel="noreferrer" className="hidden group-hover:block">
@ -193,14 +157,14 @@ export default function BreakdownModal({
return ( return (
<tr className="text-sm dark:text-gray-200" key={item.name}> <tr className="text-sm dark:text-gray-200" key={item.name}>
<td className="p-2 truncate flex items-center group"> <td className="p-2 truncate flex items-center group">
{ maybeRenderIcon(item) } {maybeRenderIcon(item)}
<FilterLink <FilterLink
pathname={`/${encodeURIComponent(site.domain)}`} pathname={`/${encodeURIComponent(site.domain)}`}
filterInfo={getFilterInfo(item)} filterInfo={getFilterInfo(item)}
> >
{trimURL(item.name, 40)} {trimURL(item.name, 40)}
</FilterLink> </FilterLink>
{ maybeRenderExternalLink(item) } {maybeRenderExternalLink(item)}
</td> </td>
{metrics.map((metric) => { {metrics.map((metric) => {
return ( return (
@ -215,7 +179,7 @@ export default function BreakdownModal({
function renderInitialLoadingSpinner() { function renderInitialLoadingSpinner() {
return ( return (
<div className="w-full h-full flex flex-col justify-center" style={{minHeight: `${MIN_HEIGHT_PX}px`}}> <div className="w-full h-full flex flex-col justify-center" style={{ minHeight: `${MIN_HEIGHT_PX}px` }}>
<div className="mx-auto loading"><div></div></div> <div className="mx-auto loading"><div></div></div>
</div> </div>
) )
@ -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>
@ -289,16 +257,16 @@ export default function BreakdownModal({
<div className="w-full h-full"> <div className="w-full h-full">
<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>
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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