Skip to content

Commit

Permalink
Table cards now paginate independently (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
benvinegar authored Jun 19, 2024
1 parent 6ce3675 commit 848569e
Show file tree
Hide file tree
Showing 14 changed files with 735 additions and 326 deletions.
103 changes: 83 additions & 20 deletions app/analytics/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,9 @@ export class AnalyticsEngineAPI {
column: T,
interval: string,
tz?: string,
limit?: number,
page: number = 1,
limit: number = 10,
) {
limit = limit || 10;

const intervalSql = intervalToSql(interval, tz);

const _column = ColumnMappings[column];
Expand All @@ -320,7 +319,7 @@ export class AnalyticsEngineAPI {
AND ${ColumnMappings.siteId} = '${siteId}'
GROUP BY ${_column}
ORDER BY count DESC
LIMIT ${limit}`;
LIMIT ${limit * page}`;

type SelectionSet = {
count: number;
Expand All @@ -342,8 +341,16 @@ export class AnalyticsEngineAPI {

const responseData =
(await response.json()) as AnalyticsQueryResult<SelectionSet>;

// since CF AE doesn't support OFFSET clauses, we select up to LIMIT and
// then slice that into the individual requested page
const pageData = responseData.data.slice(
limit * (page - 1),
limit * page,
);

resolve(
responseData.data.map((row) => {
pageData.map((row) => {
const key =
row[_column] === "" ? "(none)" : row[_column];
return [key, row["count"]] as const;
Expand All @@ -359,11 +366,9 @@ export class AnalyticsEngineAPI {
column: T,
interval: string,
tz?: string,
limit?: number,
page: number = 1,
limit: number = 10,
) {
// defaults to 1 day if not specified
limit = limit || 10;

const intervalSql = intervalToSql(interval, tz);

const _column = ColumnMappings[column];
Expand All @@ -377,7 +382,7 @@ export class AnalyticsEngineAPI {
AND ${ColumnMappings.siteId} = '${siteId}'
GROUP BY ${_column}, ${ColumnMappings.newVisitor}, ${ColumnMappings.newSession}
ORDER BY count DESC
LIMIT ${limit}`;
LIMIT ${limit * page}`;

type SelectionSet = {
readonly count: number;
Expand All @@ -401,7 +406,14 @@ export class AnalyticsEngineAPI {
const responseData =
(await response.json()) as AnalyticsQueryResult<SelectionSet>;

const result = responseData.data.reduce(
// since CF AE doesn't support OFFSET clauses, we select up to LIMIT and
// then slice that into the individual requested page
const pageData = responseData.data.slice(
limit * (page - 1),
limit * page,
);

const result = pageData.reduce(
(acc, row) => {
const key =
row[_column] === ""
Expand All @@ -426,12 +438,18 @@ export class AnalyticsEngineAPI {
return returnPromise;
}

async getCountByPath(siteId: string, interval: string, tz?: string) {
async getCountByPath(
siteId: string,
interval: string,
tz?: string,
page: number = 1,
) {
const allCountsResultPromise = this.getAllCountsByColumn(
siteId,
"path",
interval,
tz,
page,
);

return allCountsResultPromise.then((allCountsResult) => {
Expand All @@ -445,32 +463,77 @@ export class AnalyticsEngineAPI {
});
}

async getCountByUserAgent(siteId: string, interval: string, tz?: string) {
return this.getVisitorCountByColumn(siteId, "userAgent", interval, tz);
async getCountByUserAgent(
siteId: string,
interval: string,
tz?: string,
page: number = 1,
) {
return this.getVisitorCountByColumn(
siteId,
"userAgent",
interval,
tz,
page,
);
}

async getCountByCountry(siteId: string, interval: string, tz?: string) {
return this.getVisitorCountByColumn(siteId, "country", interval, tz);
async getCountByCountry(
siteId: string,
interval: string,
tz?: string,
page: number = 1,
) {
return this.getVisitorCountByColumn(
siteId,
"country",
interval,
tz,
page,
);
}

async getCountByReferrer(siteId: string, interval: string, tz?: string) {
return this.getVisitorCountByColumn(siteId, "referrer", interval, tz);
async getCountByReferrer(
siteId: string,
interval: string,
tz?: string,
page: number = 1,
) {
return this.getVisitorCountByColumn(
siteId,
"referrer",
interval,
tz,
page,
);
}
async getCountByBrowser(siteId: string, interval: string, tz?: string) {
async getCountByBrowser(
siteId: string,
interval: string,
tz?: string,
page: number = 1,
) {
return this.getVisitorCountByColumn(
siteId,
"browserName",
interval,
tz,
page,
);
}

async getCountByDevice(siteId: string, interval: string, tz?: string) {
async getCountByDevice(
siteId: string,
interval: string,
tz?: string,
page: number = 1,
) {
return this.getVisitorCountByColumn(
siteId,
"deviceModel",
interval,
tz,
page,
);
}

Expand Down
65 changes: 65 additions & 0 deletions app/components/PaginatedTableCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useEffect } from "react";
import TableCard from "~/components/TableCard";

import { Card } from "./ui/card";
import PaginationButtons from "./PaginationButtons";

const ReferrerCard = ({
siteId,
interval,
dataFetcher,
columnHeaders,
loaderUrl,
}: {
siteId: string;
interval: string;
dataFetcher: any; // ignore type for now

Check warning on line 16 in app/components/PaginatedTableCard.tsx

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
columnHeaders: string[];
loaderUrl: string;
}) => {
const countsByProperty = dataFetcher.data?.countsByProperty || [];
const page = dataFetcher.data?.page || 1;

useEffect(() => {
if (dataFetcher.state === "idle") {
dataFetcher.load(
`${loaderUrl}?site=${siteId}&interval=${interval}`,
);
}
}, []);

Check warning on line 29 in app/components/PaginatedTableCard.tsx

View workflow job for this annotation

GitHub Actions / test

React Hook useEffect has missing dependencies: 'dataFetcher', 'interval', 'loaderUrl', and 'siteId'. Either include them or remove the dependency array

useEffect(() => {
if (dataFetcher.state === "idle") {
dataFetcher.load(
`${loaderUrl}?site=${siteId}&interval=${interval}`,
);
}
}, [siteId, interval]);

Check warning on line 37 in app/components/PaginatedTableCard.tsx

View workflow job for this annotation

GitHub Actions / test

React Hook useEffect has missing dependencies: 'dataFetcher' and 'loaderUrl'. Either include them or remove the dependency array

function handlePagination(page: number) {
dataFetcher.load(
`${loaderUrl}?site=${siteId}&interval=${interval}&page=${page}`,
);
}

const hasMore = countsByProperty.length === 10;
return (
<Card className={dataFetcher.state === "loading" ? "opacity-60" : ""}>
{countsByProperty ? (
<div className="grid grid-rows-[auto,40px] h-full">
<TableCard
countByProperty={countsByProperty}
columnHeaders={columnHeaders}
/>
<PaginationButtons
page={page}
hasMore={hasMore}
handlePagination={handlePagination}
/>
</div>
) : null}
</Card>
);
};

export default ReferrerCard;
47 changes: 47 additions & 0 deletions app/components/PaginationButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react";

import { ArrowLeft, ArrowRight } from "lucide-react";

interface PaginationButtonsProps {
page: number;
hasMore: boolean;
handlePagination: (page: number) => void;
}

const PaginationButtons: React.FC<PaginationButtonsProps> = ({
page,
hasMore,
handlePagination,
}) => {
return (
<div className="p-2 pr-0 grid grid-cols-[auto,2rem,2rem] text-right">
<div></div>
<button
onClick={() => {
if (page > 1) handlePagination(page - 1);
}}
className={
page > 1
? `text-primary hover:cursor-pointer`
: `text-orange-300`
}
>
<ArrowLeft />
</button>
<button
onClick={() => {
if (hasMore) handlePagination(page + 1);
}}
className={
hasMore
? "text-primary hover:cursor-pointer"
: "text-orange-300"
}
>
<ArrowRight />
</button>
</div>
);
};

export default PaginationButtons;
78 changes: 37 additions & 41 deletions app/components/TableCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import {
TableRow,
} from "~/components/ui/table";

import { Card } from "~/components/ui/card";

type CountByProperty = [string, string][];

function calculateCountPercentages(countByProperty: CountByProperty) {
Expand Down Expand Up @@ -40,49 +38,47 @@ export default function TableCard({
? "grid-cols-[minmax(0,1fr),minmax(0,8ch),minmax(0,8ch)]"
: "grid-cols-[minmax(0,1fr),minmax(0,8ch)]";
return (
<Card>
<Table>
<TableHeader>
<TableRow className={`${gridCols}`}>
{(columnHeaders || []).map((header: string, index) => (
<TableHead
key={header}
className={
index === 0
? "text-left"
: "text-right pr-4 pl-0"
}
>
{header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{(countByProperty || []).map((item, key) => (
<TableRow
key={item[0]}
className={`group [&_td]:last:rounded-b-md ${gridCols}`}
width={barChartPercentages[key]}
<Table>
<TableHeader>
<TableRow className={`${gridCols}`}>
{(columnHeaders || []).map((header: string, index) => (
<TableHead
key={header}
className={
index === 0
? "text-left"
: "text-right pr-4 pl-0"
}
>
<TableCell className="font-medium min-w-48 break-all">
{item[0]}
</TableCell>
{header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{(countByProperty || []).map((item, key) => (
<TableRow
key={item[0]}
className={`group [&_td]:last:rounded-b-md ${gridCols}`}
width={barChartPercentages[key]}
>
<TableCell className="font-medium min-w-48 break-all">
{item[0]}
</TableCell>

<TableCell className="text-right min-w-16">
{countFormatter.format(item[1] as number)}
</TableCell>

{item.length > 2 && (
<TableCell className="text-right min-w-16">
{countFormatter.format(item[1] as number)}
{countFormatter.format(item[2] as number)}
</TableCell>

{item.length > 2 && (
<TableCell className="text-right min-w-16">
{countFormatter.format(item[2] as number)}
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</Card>
)}
</TableRow>
))}
</TableBody>
</Table>
);
}

Expand Down
Loading

0 comments on commit 848569e

Please sign in to comment.