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 { useSiteContext } from './site-context';
|
||||
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function ScrollToTop() {
|
||||
const location = useLocation();
|
||||
|
||||
@ -30,45 +43,47 @@ function ScrollToTop() {
|
||||
export default function Router() {
|
||||
const site = useSiteContext()
|
||||
return (
|
||||
<BrowserRouter basename={site.shared ? `/share/${encodeURIComponent(site.domain)}` : encodeURIComponent(site.domain)}>
|
||||
<QueryContextProvider>
|
||||
<Route path="/">
|
||||
<ScrollToTop />
|
||||
<Dashboard />
|
||||
<Switch>
|
||||
<Route exact path={["/sources", "/utm_mediums", "/utm_sources", "/utm_campaigns", "/utm_contents", "/utm_terms"]}>
|
||||
<SourcesModal />
|
||||
</Route>
|
||||
<Route exact path="/referrers/Google">
|
||||
<GoogleKeywordsModal site={site} />
|
||||
</Route>
|
||||
<Route exact path="/referrers/:referrer">
|
||||
<ReferrersDrilldownModal />
|
||||
</Route>
|
||||
<Route path="/pages">
|
||||
<PagesModal />
|
||||
</Route>
|
||||
<Route path="/entry-pages">
|
||||
<EntryPagesModal />
|
||||
</Route>
|
||||
<Route path="/exit-pages">
|
||||
<ExitPagesModal />
|
||||
</Route>
|
||||
<Route exact path={["/countries", "/regions", "/cities"]}>
|
||||
<LocationsModal />
|
||||
</Route>
|
||||
<Route path="/custom-prop-values/:prop_key">
|
||||
<PropsModal />
|
||||
</Route>
|
||||
<Route path="/conversions">
|
||||
<ConversionsModal />
|
||||
</Route>
|
||||
<Route path={["/filter/:field"]}>
|
||||
<FilterModal site={site} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Route>
|
||||
</QueryContextProvider>
|
||||
</BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter basename={site.shared ? `/share/${encodeURIComponent(site.domain)}` : encodeURIComponent(site.domain)}>
|
||||
<QueryContextProvider>
|
||||
<Route path="/">
|
||||
<ScrollToTop />
|
||||
<Dashboard />
|
||||
<Switch>
|
||||
<Route exact path={["/sources", "/utm_mediums", "/utm_sources", "/utm_campaigns", "/utm_contents", "/utm_terms"]}>
|
||||
<SourcesModal />
|
||||
</Route>
|
||||
<Route exact path="/referrers/Google">
|
||||
<GoogleKeywordsModal site={site} />
|
||||
</Route>
|
||||
<Route exact path="/referrers/:referrer">
|
||||
<ReferrersDrilldownModal />
|
||||
</Route>
|
||||
<Route path="/pages">
|
||||
<PagesModal />
|
||||
</Route>
|
||||
<Route path="/entry-pages">
|
||||
<EntryPagesModal />
|
||||
</Route>
|
||||
<Route path="/exit-pages">
|
||||
<ExitPagesModal />
|
||||
</Route>
|
||||
<Route exact path={["/countries", "/regions", "/cities"]}>
|
||||
<LocationsModal />
|
||||
</Route>
|
||||
<Route path="/custom-prop-values/:prop_key">
|
||||
<PropsModal />
|
||||
</Route>
|
||||
<Route path="/conversions">
|
||||
<ConversionsModal />
|
||||
</Route>
|
||||
<Route path={["/filter/:field"]}>
|
||||
<FilterModal site={site} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Route>
|
||||
</QueryContextProvider>
|
||||
</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 { FilterLink } from "../reports/list";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
|
||||
const LIMIT = 100
|
||||
import { useDebounce } from "../../custom-hooks";
|
||||
import { useAPIClient } from "../../hooks/api-client";
|
||||
const MIN_HEIGHT_PX = 500
|
||||
|
||||
// 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
|
||||
|
||||
// * `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.
|
||||
|
||||
@ -84,24 +83,36 @@ export default function BreakdownModal({
|
||||
addSearchFilter,
|
||||
getFilterInfo
|
||||
}) {
|
||||
const {query} = useQueryContext();
|
||||
const searchBoxRef = useRef(null)
|
||||
const { query } = useQueryContext();
|
||||
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 [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
|
||||
|
||||
let queryWithSearchFilter = {...query}
|
||||
|
||||
useMountedEffect(() => { debouncedFetchData() }, [search])
|
||||
if (searchEnabled && search !== '') {
|
||||
queryWithSearchFilter = addSearchFilter(query, search)
|
||||
}
|
||||
|
||||
return [queryWithSearchFilter, {detailed: true}]
|
||||
},
|
||||
afterFetchData,
|
||||
afterFetchNextPage
|
||||
})
|
||||
|
||||
useMountedEffect(() => { fetchNextPage() }, [page])
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchEnabled) { return }
|
||||
@ -120,54 +131,7 @@ export default function BreakdownModal({
|
||||
return () => {
|
||||
searchBox.removeEventListener('keyup', handleKeyUp);
|
||||
}
|
||||
}, [])
|
||||
|
||||
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)
|
||||
}
|
||||
}, [searchEnabled])
|
||||
|
||||
function maybeRenderIcon(item) {
|
||||
if (typeof renderIcon === 'function') {
|
||||
@ -178,8 +142,8 @@ export default function BreakdownModal({
|
||||
function maybeRenderExternalLink(item) {
|
||||
if (typeof getExternalLinkURL === 'function') {
|
||||
const linkUrl = getExternalLinkURL(item)
|
||||
|
||||
if (!linkUrl) { return null}
|
||||
|
||||
if (!linkUrl) { return null }
|
||||
|
||||
return (
|
||||
<a target="_blank" href={linkUrl} rel="noreferrer" className="hidden group-hover:block">
|
||||
@ -193,14 +157,14 @@ export default function BreakdownModal({
|
||||
return (
|
||||
<tr className="text-sm dark:text-gray-200" key={item.name}>
|
||||
<td className="p-2 truncate flex items-center group">
|
||||
{ maybeRenderIcon(item) }
|
||||
{maybeRenderIcon(item)}
|
||||
<FilterLink
|
||||
pathname={`/${encodeURIComponent(site.domain)}`}
|
||||
filterInfo={getFilterInfo(item)}
|
||||
>
|
||||
{trimURL(item.name, 40)}
|
||||
</FilterLink>
|
||||
{ maybeRenderExternalLink(item) }
|
||||
{maybeRenderExternalLink(item)}
|
||||
</td>
|
||||
{metrics.map((metric) => {
|
||||
return (
|
||||
@ -215,7 +179,7 @@ export default function BreakdownModal({
|
||||
|
||||
function renderInitialLoadingSpinner() {
|
||||
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>
|
||||
)
|
||||
@ -228,11 +192,13 @@ export default function BreakdownModal({
|
||||
}
|
||||
|
||||
function renderLoadMoreButton() {
|
||||
if (isPending) return null
|
||||
if (!isFetching && !hasNextPage) return null
|
||||
|
||||
return (
|
||||
<div className="w-full text-center my-4">
|
||||
<button onClick={loadNextPage} type="button" className="button">
|
||||
Load more
|
||||
</button>
|
||||
<div className="flex flex-col w-full my-4 items-center justify-center h-10">
|
||||
{!isFetching && <button onClick={fetchNextPage} type="button" className="button">Load more</button>}
|
||||
{isFetchingNextPage && renderSmallLoadingSpinner()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -241,6 +207,8 @@ export default function BreakdownModal({
|
||||
setSearch(e.target.value)
|
||||
}
|
||||
|
||||
const debouncedHandleInputChange = useDebounce(handleInputChange)
|
||||
|
||||
function renderSearchInput() {
|
||||
return (
|
||||
<input
|
||||
@ -248,13 +216,13 @@ export default function BreakdownModal({
|
||||
type="text"
|
||||
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"
|
||||
onChange={handleInputChange}
|
||||
onChange={debouncedHandleInputChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function renderModalBody() {
|
||||
if (results) {
|
||||
if (data?.pages?.length) {
|
||||
return (
|
||||
<main className="modal__content">
|
||||
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
|
||||
@ -277,7 +245,7 @@ export default function BreakdownModal({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ results.map(renderRow) }
|
||||
{data.pages.map((p) => p.map(renderRow))}
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
@ -289,16 +257,16 @@ export default function BreakdownModal({
|
||||
<div className="w-full h-full">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<h1 className="text-xl font-bold dark:text-gray-100">{ reportInfo.title }</h1>
|
||||
{ !initialLoading && loading && renderSmallLoadingSpinner() }
|
||||
<h1 className="text-xl font-bold dark:text-gray-100">{reportInfo.title}</h1>
|
||||
{!isPending && isFetching && renderSmallLoadingSpinner()}
|
||||
</div>
|
||||
{ searchEnabled && renderSearchInput()}
|
||||
{searchEnabled && renderSearchInput()}
|
||||
</div>
|
||||
<div className="my-4 border-b border-gray-300"></div>
|
||||
<div style={{minHeight: `${MIN_HEIGHT_PX}px`}}>
|
||||
{ initialLoading && renderInitialLoadingSpinner() }
|
||||
{ !initialLoading && renderModalBody() }
|
||||
{ !loading && moreResultsAvailable && renderLoadMoreButton() }
|
||||
<div style={{ minHeight: `${MIN_HEIGHT_PX}px` }}>
|
||||
{isPending && renderInitialLoadingSpinner()}
|
||||
{!isPending && renderModalBody()}
|
||||
{renderLoadMoreButton()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -3,15 +3,18 @@ import React, { useCallback, useState } from "react";
|
||||
import Modal from './modal'
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from "../reports/metrics";
|
||||
import * as url from '../../util/url';
|
||||
import { useSiteContext } from "../../site-context";
|
||||
|
||||
/*global BUILD_EXTRA*/
|
||||
function ConversionsModal() {
|
||||
const [showRevenue, setShowRevenue] = useState(false)
|
||||
const site = useSiteContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: 'Goal Conversions',
|
||||
dimension: 'goal',
|
||||
endpoint: '/conversions',
|
||||
endpoint: url.apiPath(site, '/conversions'),
|
||||
dimensionLabel: "Goal"
|
||||
}
|
||||
|
||||
@ -20,7 +23,7 @@ function ConversionsModal() {
|
||||
prefix: reportInfo.dimension,
|
||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||
}
|
||||
}, [])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
@ -37,7 +40,7 @@ function ConversionsModal() {
|
||||
// whether revenue metrics are passed into BreakdownModal in `metrics`.
|
||||
const afterFetchData = useCallback((res) => {
|
||||
setShowRevenue(revenueInResponse(res))
|
||||
}, [showRevenue])
|
||||
}, [])
|
||||
|
||||
// After fetching the next page, we never want to set `showRevenue` to
|
||||
// `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 BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from '../reports/metrics'
|
||||
import * as url from '../../util/url';
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
|
||||
function EntryPagesModal() {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: 'Entry Pages',
|
||||
dimension: 'entry_page',
|
||||
endpoint: '/entry-pages',
|
||||
endpoint: url.apiPath(site, '/entry-pages'),
|
||||
dimensionLabel: 'Entry page'
|
||||
}
|
||||
|
||||
@ -21,11 +24,11 @@ function EntryPagesModal() {
|
||||
prefix: reportInfo.dimension,
|
||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||
}
|
||||
}, [])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const addSearchFilter = useCallback((query, searchString) => {
|
||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||
}, [])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
function chooseMetrics() {
|
||||
if (hasGoalFilter(query)) {
|
||||
|
@ -4,15 +4,18 @@ import { hasGoalFilter } from "../../util/filters";
|
||||
import { addFilter } from '../../query'
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from '../reports/metrics'
|
||||
import * as url from '../../util/url';
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
|
||||
function ExitPagesModal() {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: 'Exit Pages',
|
||||
dimension: 'exit_page',
|
||||
endpoint: '/exit-pages',
|
||||
endpoint: url.apiPath(site, '/exit-pages'),
|
||||
dimensionLabel: 'Page url'
|
||||
}
|
||||
|
||||
@ -21,11 +24,11 @@ function ExitPagesModal() {
|
||||
prefix: reportInfo.dimension,
|
||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||
}
|
||||
}, [])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const addSearchFilter = useCallback((query, searchString) => {
|
||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||
}, [])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
function chooseMetrics() {
|
||||
if (hasGoalFilter(query)) {
|
||||
|
@ -5,7 +5,9 @@ import Modal from './modal'
|
||||
import { hasGoalFilter } from "../../util/filters";
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from "../reports/metrics";
|
||||
import * as url from '../../util/url';
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
|
||||
const VIEWS = {
|
||||
countries: { title: 'Top Countries', dimension: 'country', endpoint: '/countries', dimensionLabel: 'Country' },
|
||||
@ -15,18 +17,20 @@ const VIEWS = {
|
||||
|
||||
function LocationsModal({ location }) {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const urlParts = location.pathname.split('/')
|
||||
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) => {
|
||||
return {
|
||||
prefix: reportInfo.dimension,
|
||||
filter: ["is", reportInfo.dimension, [listItem.code]]
|
||||
}
|
||||
}, [])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
function chooseMetrics() {
|
||||
if (hasGoalFilter(query)) {
|
||||
|
@ -4,15 +4,18 @@ import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
||||
import { addFilter } from '../../query'
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from '../reports/metrics'
|
||||
import * as url from '../../util/url';
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
|
||||
function PagesModal() {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: 'Top Pages',
|
||||
dimension: 'page',
|
||||
endpoint: '/pages',
|
||||
endpoint: url.apiPath(site, '/pages'),
|
||||
dimensionLabel: 'Page url'
|
||||
}
|
||||
|
||||
@ -21,11 +24,11 @@ function PagesModal() {
|
||||
prefix: reportInfo.dimension,
|
||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||
}
|
||||
}, [])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const addSearchFilter = useCallback((query, searchString) => {
|
||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||
}, [])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
function chooseMetrics() {
|
||||
if (hasGoalFilter(query)) {
|
||||
|
@ -7,6 +7,7 @@ import { specialTitleWhenGoalFilter } from "../behaviours/goal-conversions";
|
||||
import { EVENT_PROPS_PREFIX, hasGoalFilter } from "../../util/filters"
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from "../reports/metrics";
|
||||
import * as url from "../../util/url";
|
||||
import { revenueAvailable } from "../../query";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
@ -22,7 +23,7 @@ function PropsModal({ location }) {
|
||||
const reportInfo = {
|
||||
title: specialTitleWhenGoalFilter(query, 'Custom Property Breakdown'),
|
||||
dimension: propKey,
|
||||
endpoint: `/custom-prop-values/${propKey}`,
|
||||
endpoint: url.apiPath(site, `/custom-prop-values/${propKey}`),
|
||||
dimensionLabel: propKey
|
||||
}
|
||||
|
||||
@ -31,11 +32,11 @@ function PropsModal({ location }) {
|
||||
prefix: `${EVENT_PROPS_PREFIX}${propKey}`,
|
||||
filter: ["is", `${EVENT_PROPS_PREFIX}${propKey}`, [listItem.name]]
|
||||
}
|
||||
}, [])
|
||||
}, [propKey])
|
||||
|
||||
const addSearchFilter = useCallback((query, searchString) => {
|
||||
return addFilter(query, ['contains', `${EVENT_PROPS_PREFIX}${propKey}`, [searchString]])
|
||||
}, [])
|
||||
}, [propKey])
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
|
@ -5,16 +5,19 @@ import Modal from './modal'
|
||||
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from "../reports/metrics";
|
||||
import * as url from "../../util/url";
|
||||
import { addFilter } from "../../query";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
|
||||
function ReferrerDrilldownModal({ match }) {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: "Referrer Drilldown",
|
||||
dimension: 'referrer',
|
||||
endpoint: `/referrers/${match.params.referrer}`,
|
||||
endpoint: url.apiPath(site, `/referrers/${match.params.referrer}`),
|
||||
dimensionLabel: "Referrer"
|
||||
}
|
||||
|
||||
@ -23,11 +26,11 @@ function ReferrerDrilldownModal({ match }) {
|
||||
prefix: reportInfo.dimension,
|
||||
filter: ['is', reportInfo.dimension, [listItem.name]]
|
||||
}
|
||||
}, [])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const addSearchFilter = useCallback((query, searchString) => {
|
||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||
}, [])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
function chooseMetrics() {
|
||||
if (hasGoalFilter(query)) {
|
||||
|
@ -5,8 +5,10 @@ import Modal from './modal'
|
||||
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from "../reports/metrics";
|
||||
import * as url from "../../util/url";
|
||||
import { addFilter } from "../../query";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
|
||||
const VIEWS = {
|
||||
sources: {
|
||||
@ -39,22 +41,24 @@ const VIEWS = {
|
||||
|
||||
function SourcesModal({ location }) {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const urlParts = location.pathname.split('/')
|
||||
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) => {
|
||||
return {
|
||||
prefix: reportInfo.dimension,
|
||||
filter: ["is", reportInfo.dimension, [listItem.name]]
|
||||
}
|
||||
}, [])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
const addSearchFilter = useCallback((query, searchString) => {
|
||||
return addFilter(query, ['contains', reportInfo.dimension, [searchString]])
|
||||
}, [])
|
||||
}, [reportInfo.dimension])
|
||||
|
||||
function chooseMetrics() {
|
||||
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/forms": "^0.5.6",
|
||||
"@tailwindcss/typography": "^0.4.1",
|
||||
"@tanstack/react-query": "^5.51.1",
|
||||
"abortcontroller-polyfill": "^1.7.3",
|
||||
"alpinejs": "^3.13.1",
|
||||
"chart.js": "^3.3.2",
|
||||
@ -446,6 +447,30 @@
|
||||
"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": {
|
||||
"version": "3.5.38",
|
||||
"license": "MIT"
|
||||
|
@ -17,6 +17,7 @@
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/forms": "^0.5.6",
|
||||
"@tailwindcss/typography": "^0.4.1",
|
||||
"@tanstack/react-query": "^5.51.1",
|
||||
"abortcontroller-polyfill": "^1.7.3",
|
||||
"alpinejs": "^3.13.1",
|
||||
"chart.js": "^3.3.2",
|
||||
|
Loading…
Reference in New Issue
Block a user