diff --git a/apps/dashboard/src/@/api/analytics.ts b/apps/dashboard/src/@/api/analytics.ts index a6f2819e1c6..39a89144148 100644 --- a/apps/dashboard/src/@/api/analytics.ts +++ b/apps/dashboard/src/@/api/analytics.ts @@ -17,6 +17,29 @@ import type { import { getAuthToken } from "./auth-token"; import { getChains } from "./chain"; +export interface InsightChainStats { + date: string; + chainId: string; + totalRequests: number; +} + +export interface InsightStatusCodeStats { + date: string; + httpStatusCode: number; + totalRequests: number; +} + +export interface InsightEndpointStats { + date: string; + endpoint: string; + totalRequests: number; +} + +interface InsightUsageStats { + date: string; + totalRequests: number; +} + async function fetchAnalytics( input: string | URL, init?: RequestInit, @@ -76,6 +99,9 @@ function buildSearchParams(params: AnalyticsQueryParams): URLSearchParams { if (params.period) { searchParams.append("period", params.period); } + if (params.limit) { + searchParams.append("limit", params.limit.toString()); + } return searchParams; } @@ -424,3 +450,91 @@ export async function getEngineCloudMethodUsage( const json = await res.json(); return json.data as EngineCloudStats[]; } + +export async function getInsightChainUsage( + params: AnalyticsQueryParams, +): Promise<{ data: InsightChainStats[] } | { errorMessage: string }> { + const searchParams = buildSearchParams(params); + const res = await fetchAnalytics( + `v2/insight/usage/by-chain?${searchParams.toString()}`, + { + method: "GET", + }, + ); + + if (res?.status !== 200) { + const reason = await res?.text(); + const errMsg = `Failed to fetch Insight chain usage: ${res?.status} - ${res.statusText} - ${reason}`; + console.error(errMsg); + return { errorMessage: errMsg }; + } + + const json = await res.json(); + return { data: json.data as InsightChainStats[] }; +} + +export async function getInsightStatusCodeUsage( + params: AnalyticsQueryParams, +): Promise<{ data: InsightStatusCodeStats[] } | { errorMessage: string }> { + const searchParams = buildSearchParams(params); + const res = await fetchAnalytics( + `v2/insight/usage/by-status-code?${searchParams.toString()}`, + { + method: "GET", + }, + ); + + if (res?.status !== 200) { + const reason = await res?.text(); + const errMsg = `Failed to fetch Insight status code usage: ${res?.status} - ${res.statusText} - ${reason}`; + console.error(errMsg); + return { errorMessage: errMsg }; + } + + const json = await res.json(); + return { data: json.data as InsightStatusCodeStats[] }; +} + +export async function getInsightEndpointUsage( + params: AnalyticsQueryParams, +): Promise<{ data: InsightEndpointStats[] } | { errorMessage: string }> { + const searchParams = buildSearchParams(params); + const res = await fetchAnalytics( + `v2/insight/usage/by-endpoint?${searchParams.toString()}`, + { + method: "GET", + }, + ); + + if (res?.status !== 200) { + const reason = await res?.text(); + const errMsg = `Failed to fetch Insight endpoint usage: ${res?.status} - ${res.statusText} - ${reason}`; + console.error(errMsg); + return { errorMessage: errMsg }; + } + + const json = await res.json(); + return { data: json.data as InsightEndpointStats[] }; +} + +export async function getInsightUsage( + params: AnalyticsQueryParams, +): Promise<{ data: InsightUsageStats[] } | { errorMessage: string }> { + const searchParams = buildSearchParams(params); + const res = await fetchAnalytics( + `v2/insight/usage?${searchParams.toString()}`, + { + method: "GET", + }, + ); + + if (res?.status !== 200) { + const reason = await res?.text(); + const errMsg = `Failed to fetch Insight usage: ${res?.status} - ${res.statusText} - ${reason}`; + console.error(errMsg); + return { errorMessage: errMsg }; + } + + const json = await res.json(); + return { data: json.data as InsightUsageStats[] }; +} diff --git a/apps/dashboard/src/@/types/analytics.ts b/apps/dashboard/src/@/types/analytics.ts index 895da7b95df..d294b537cde 100644 --- a/apps/dashboard/src/@/types/analytics.ts +++ b/apps/dashboard/src/@/types/analytics.ts @@ -78,4 +78,5 @@ export interface AnalyticsQueryParams { from?: Date; to?: Date; period?: "day" | "week" | "month" | "year" | "all"; + limit?: number; } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/blueprint-card.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/blueprint-card.tsx deleted file mode 100644 index 79d750da614..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/blueprint-card.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { - Code2Icon, - DatabaseIcon, - ExternalLinkIcon, - ZapIcon, -} from "lucide-react"; -import Link from "next/link"; -import { Button } from "@/components/ui/button"; - -export function BlueprintCard() { - const features = [ - { - description: "RESTful endpoints for any application", - icon: Code2Icon, - title: "Easy-to-Use API", - }, - { - description: - "No need to index blockchains yourself or manage infrastructure and RPC costs.", - icon: DatabaseIcon, - title: "Managed Infrastructure", - }, - { - description: "Access any transaction, event or token API data", - icon: ZapIcon, - title: "Lightning-Fast Queries", - }, - ]; - - return ( -
- {/* header */} -
-
-

Blueprints

- -
- -
-
-
- - {/* Content */} -
-

- Simple endpoints for querying rich blockchain data -

-

- A blueprint is an API that provides access to on-chain data in a - user-friendly format.
No need for ABIs, decoding, RPC, or web3 - knowledge required to fetch blockchain data. -

- -
- - {/* Features */} -
- {features.map((feature) => ( -
-
- -
-
-

{feature.title}

-

- {feature.description} -

-
-
- ))} -
-
- - {/* Playground link */} -
- -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/InsightAnalytics.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/InsightAnalytics.tsx new file mode 100644 index 00000000000..40f2b0b35cb --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/InsightAnalytics.tsx @@ -0,0 +1,223 @@ +import "server-only"; + +import { ActivityIcon, AlertCircleIcon, CloudAlertIcon } from "lucide-react"; +import { ResponsiveSuspense } from "responsive-rsc"; +import type { ThirdwebClient } from "thirdweb"; +import { + getInsightChainUsage, + getInsightEndpointUsage, + getInsightStatusCodeUsage, + getInsightUsage, +} from "@/api/analytics"; +import type { Range } from "@/components/analytics/date-range-selector"; +import { StatCard } from "@/components/analytics/stat"; +import { Card, CardContent } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { InsightAnalyticsFilter } from "./InsightAnalyticsFilter"; +import { InsightFTUX } from "./insight-ftux"; +import { RequestsByStatusGraph } from "./RequestsByStatusGraph"; +import { TopInsightChainsTable } from "./TopChainsTable"; +import { TopInsightEndpointsTable } from "./TopEndpointsTable"; + +// Error state component for analytics +function AnalyticsErrorState({ + title, + message, + className, +}: { + title: string; + message: string; + className?: string; +}) { + return ( + + +
+ +
+
+

{title}

+

{message}

+
+
+
+ ); +} + +export async function InsightAnalytics(props: { + projectClientId: string; + client: ThirdwebClient; + projectId: string; + teamId: string; + range: Range; + interval: "day" | "week"; +}) { + const { projectId, teamId, range, interval } = props; + + const allTimeRequestsPromise = getInsightUsage({ + from: range.from, + period: "all", + projectId: projectId, + teamId: teamId, + to: range.to, + }); + const chainsDataPromise = getInsightChainUsage({ + from: range.from, + limit: 10, + period: "all", + projectId: projectId, + teamId: teamId, + to: range.to, + }); + const statusCodesDataPromise = getInsightStatusCodeUsage({ + from: range.from, + period: interval, + projectId: projectId, + teamId: teamId, + to: range.to, + }); + const endpointsDataPromise = getInsightEndpointUsage({ + from: range.from, + limit: 10, + period: "all", + projectId: projectId, + teamId: teamId, + to: range.to, + }); + + const [allTimeRequestsData, statusCodesData, endpointsData, chainsData] = + await Promise.all([ + allTimeRequestsPromise, + statusCodesDataPromise, + endpointsDataPromise, + chainsDataPromise, + ]); + + const hasVolume = + "data" in allTimeRequestsData && + allTimeRequestsData.data?.some((d) => d.totalRequests > 0); + + const allTimeRequests = + "data" in allTimeRequestsData + ? allTimeRequestsData.data?.reduce( + (acc, curr) => acc + curr.totalRequests, + 0, + ) + : 0; + + let requestsInPeriod = 0; + let errorsInPeriod = 0; + if ("data" in statusCodesData) { + for (const request of statusCodesData.data) { + requestsInPeriod += request.totalRequests; + if (request.httpStatusCode >= 400) { + errorsInPeriod += request.totalRequests; + } + } + } + const errorRate = Number( + ((errorsInPeriod / (requestsInPeriod || 1)) * 100).toFixed(2), + ); + + if (!hasVolume) { + return ( +
+ +
+ ); + } + + return ( +
+
+ +
+ +
+ + +
+ +
+ + +
+
+
+ } + searchParamsUsed={["from", "to", "interval"]} + > +
+
+ + `${value}%`} + icon={CloudAlertIcon} + isPending={false} + label="Error rate" + value={errorRate} + /> +
+ + {"errorMessage" in statusCodesData ? ( + + ) : ( + + )} + + +
+ {"errorMessage" in endpointsData ? ( + + ) : ( + + )} +
+ {"errorMessage" in chainsData ? ( + + ) : ( + + )} +
+
+
+
+ ); +} + +function GridWithSeparator(props: { children: React.ReactNode }) { + return ( +
+ {props.children} + {/* Desktop - horizontal middle */} +
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/InsightAnalyticsFilter.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/InsightAnalyticsFilter.tsx new file mode 100644 index 00000000000..f62e5d43f90 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/InsightAnalyticsFilter.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { + useResponsiveSearchParams, + useSetResponsiveSearchParams, +} from "responsive-rsc"; +import { DateRangeSelector } from "@/components/analytics/date-range-selector"; +import { IntervalSelector } from "@/components/analytics/interval-selector"; +import { getFiltersFromSearchParams, normalizeTimeISOString } from "@/lib/time"; + +type SearchParams = { + from?: string; + to?: string; + interval?: "day" | "week"; +}; + +export function InsightAnalyticsFilter() { + const responsiveSearchParams = useResponsiveSearchParams(); + const setResponsiveSearchParams = useSetResponsiveSearchParams(); + + const { range, interval } = getFiltersFromSearchParams({ + defaultRange: "last-30", + from: responsiveSearchParams.from, + interval: responsiveSearchParams.interval, + to: responsiveSearchParams.to, + }); + + return ( +
+ { + setResponsiveSearchParams((v: SearchParams) => { + const newParams = { + ...v, + from: normalizeTimeISOString(newRange.from), + to: normalizeTimeISOString(newRange.to), + }; + return newParams; + }); + }} + /> + + { + setResponsiveSearchParams((v: SearchParams) => { + const newParams = { + ...v, + interval: newInterval, + }; + return newParams; + }); + }} + /> +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/RequestsByStatusGraph.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/RequestsByStatusGraph.tsx new file mode 100644 index 00000000000..1418b07043a --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/RequestsByStatusGraph.tsx @@ -0,0 +1,121 @@ +"use client"; +import { format } from "date-fns"; +import { useMemo } from "react"; +import { shortenLargeNumber } from "thirdweb/utils"; +import type { InsightStatusCodeStats } from "@/api/analytics"; +import { EmptyChartState } from "@/components/analytics/empty-chart-state"; +import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart"; +import type { ChartConfig } from "@/components/ui/chart"; + +type ChartData = Record & { + time: string; // human readable date +}; +const defaultLabel = 200; + +export function RequestsByStatusGraph(props: { + data: InsightStatusCodeStats[]; + isPending: boolean; + title: string; + description: string; +}) { + const topStatusCodesToShow = 10; + + const { chartConfig, chartData } = useMemo(() => { + const _chartConfig: ChartConfig = {}; + const _chartDataMap: Map = new Map(); + const statusCodeToVolumeMap: Map = new Map(); + // for each stat, add it in _chartDataMap + for (const stat of props.data) { + const chartData = _chartDataMap.get(stat.date); + const { httpStatusCode } = stat; + + // if no data for current day - create new entry + if (!chartData && stat.totalRequests > 0) { + _chartDataMap.set(stat.date, { + time: stat.date, + [httpStatusCode || defaultLabel]: stat.totalRequests, + } as ChartData); + } else if (chartData) { + chartData[httpStatusCode || defaultLabel] = + (chartData[httpStatusCode || defaultLabel] || 0) + stat.totalRequests; + } + + statusCodeToVolumeMap.set( + (httpStatusCode || defaultLabel).toString(), + stat.totalRequests + + (statusCodeToVolumeMap.get( + (httpStatusCode || defaultLabel).toString(), + ) || 0), + ); + } + + const statusCodesSorted = Array.from(statusCodeToVolumeMap.entries()) + .sort((a, b) => b[1] - a[1]) + .map((w) => w[0]); + + const statusCodesToShow = statusCodesSorted.slice(0, topStatusCodesToShow); + const statusCodesAsOther = statusCodesSorted.slice(topStatusCodesToShow); + + // replace chainIdsToTagAsOther chainId with "other" + for (const data of _chartDataMap.values()) { + for (const statusCode in data) { + if (statusCodesAsOther.includes(statusCode)) { + data.others = (data.others || 0) + (data[statusCode] || 0); + delete data[statusCode]; + } + } + } + + statusCodesToShow.forEach((statusCode, i) => { + _chartConfig[statusCode] = { + color: `hsl(var(--chart-${(i % 10) + 1}))`, + label: statusCodesToShow[i], + }; + }); + + if (statusCodesAsOther.length > 0) { + _chartConfig.others = { + color: "hsl(var(--muted-foreground))", + label: "Others", + }; + } + + return { + chartConfig: _chartConfig, + chartData: Array.from(_chartDataMap.values()).sort( + (a, b) => new Date(a.time).getTime() - new Date(b.time).getTime(), + ), + }; + }, [props.data]); + + return ( + +

+ {props.title} +

+

+ {props.description} +

+
+ } + data={chartData} + emptyChartState={} + hideLabel={false} + isPending={props.isPending} + showLegend + toolTipLabelFormatter={(_v, item) => { + if (Array.isArray(item)) { + const time = item[0].payload.time as number; + return format(new Date(time), "MMM d, yyyy"); + } + return undefined; + }} + toolTipValueFormatter={(v) => shortenLargeNumber(v as number)} + variant="stacked" + /> + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/TopChainsTable.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/TopChainsTable.tsx new file mode 100644 index 00000000000..79c661299ae --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/TopChainsTable.tsx @@ -0,0 +1,106 @@ +"use client"; +import { useMemo } from "react"; +import type { ThirdwebClient } from "thirdweb"; +import { shortenLargeNumber } from "thirdweb/utils"; +import type { InsightChainStats } from "@/api/analytics"; +import { SkeletonContainer } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { CardHeading } from "../../universal-bridge/components/common"; + +export function TopInsightChainsTable(props: { + data: InsightChainStats[]; + client: ThirdwebClient; +}) { + const tableData = useMemo(() => { + return props.data.sort((a, b) => b.totalRequests - a.totalRequests); + }, [props.data]); + const isEmpty = useMemo(() => tableData.length === 0, [tableData]); + + return ( +
+ {/* header */} +
+ Top Chains +
+ +
+ + + + + Chain ID + Requests + + + + {tableData.map((chain, i) => { + return ( + + ); + })} + +
+ {isEmpty && ( +
+ No data available +
+ )} +
+
+ ); +} + +function ChainTableRow(props: { + chain?: { + chainId: string; + totalRequests: number; + }; + client: ThirdwebClient; + rowIndex: number; +}) { + const delayAnim = { + animationDelay: `${props.rowIndex * 100}ms`, + }; + + return ( + + + ( +

+ {v === "0" ? "Multichain" : v} +

+ )} + skeletonData="..." + style={delayAnim} + /> +
+ + { + return

{shortenLargeNumber(v)}

; + }} + skeletonData={0} + style={delayAnim} + /> +
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/TopEndpointsTable.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/TopEndpointsTable.tsx new file mode 100644 index 00000000000..1ee0853f08b --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/TopEndpointsTable.tsx @@ -0,0 +1,106 @@ +"use client"; +import { useMemo } from "react"; +import type { ThirdwebClient } from "thirdweb"; +import { shortenLargeNumber } from "thirdweb/utils"; +import type { InsightEndpointStats } from "@/api/analytics"; +import { SkeletonContainer } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { CardHeading } from "../../universal-bridge/components/common"; + +export function TopInsightEndpointsTable(props: { + data: InsightEndpointStats[]; + client: ThirdwebClient; +}) { + const tableData = useMemo(() => { + return props.data?.sort((a, b) => b.totalRequests - a.totalRequests); + }, [props.data]); + const isEmpty = useMemo(() => tableData.length === 0, [tableData]); + + return ( +
+ {/* header */} +
+ Top Endpoints +
+ +
+ + + + + Endpoint + Requests + + + + {tableData.map((endpoint, i) => { + return ( + + ); + })} + +
+ {isEmpty && ( +
+ No data available +
+ )} +
+
+ ); +} + +function EndpointTableRow(props: { + endpoint?: { + endpoint: string; + totalRequests: number; + }; + client: ThirdwebClient; + rowIndex: number; +}) { + const delayAnim = { + animationDelay: `${props.rowIndex * 100}ms`, + }; + + return ( + + + ( +

+ {v} +

+ )} + skeletonData="..." + style={delayAnim} + /> +
+ + { + return

{shortenLargeNumber(v)}

; + }} + skeletonData={0} + style={delayAnim} + /> +
+
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/insight-ftux.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/insight-ftux.tsx similarity index 91% rename from apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/insight-ftux.tsx rename to apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/insight-ftux.tsx index 12fc1cd34ce..22caabe269b 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/insight-ftux.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/insight-ftux.tsx @@ -1,7 +1,7 @@ import { CodeServer } from "@/components/ui/code/code.server"; import { isProd } from "@/constants/env-utils"; -import { ClientIDSection } from "../components/ProjectFTUX/ClientIDSection"; -import { WaitingForIntegrationCard } from "../components/WaitingForIntegrationCard/WaitingForIntegrationCard"; +import { ClientIDSection } from "../../components/ProjectFTUX/ClientIDSection"; +import { WaitingForIntegrationCard } from "../../components/WaitingForIntegrationCard/WaitingForIntegrationCard"; export function InsightFTUX(props: { clientId: string }) { return ( diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/layout.tsx new file mode 100644 index 00000000000..c6bbdd86800 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/layout.tsx @@ -0,0 +1,99 @@ +import { redirect } from "next/navigation"; +import { getProject } from "@/api/projects"; +import { UnderlineLink } from "@/components/ui/UnderlineLink"; +import { FooterLinksSection } from "../components/footer/FooterLinksSection"; + +export default async function Layout(props: { + params: Promise<{ + team_slug: string; + project_slug: string; + }>; + children: React.ReactNode; +}) { + const params = await props.params; + const project = await getProject(params.team_slug, params.project_slug); + + if (!project) { + redirect(`/team/${params.team_slug}`); + } + + return ( +
+
+
+

+ Insight +

+

+ APIs to retrieve blockchain data from any EVM chain, enrich it with + metadata, and transform it using custom logic.{" "} + + Learn more + +

+
+ +
+
+ +
+
+ {props.children} +
+ +
+
+
+ +
+
+
+ ); +} + +function InsightFooter() { + return ( + + ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/page.tsx index 3c55930a3d0..3e326952fa3 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/page.tsx @@ -1,109 +1,83 @@ -import { notFound } from "next/navigation"; -import { isProjectActive } from "@/api/analytics"; +import { loginRedirect } from "@app/login/loginRedirect"; +import { ArrowUpRightIcon } from "lucide-react"; +import { redirect } from "next/navigation"; +import { ResponsiveSearchParamsProvider } from "responsive-rsc"; +import { getAuthToken } from "@/api/auth-token"; import { getProject } from "@/api/projects"; -import { getTeamBySlug } from "@/api/team"; -import { FooterLinksSection } from "../components/footer/FooterLinksSection"; -import { BlueprintCard } from "./blueprint-card"; -import { InsightFTUX } from "./insight-ftux"; +import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; +import { getFiltersFromSearchParams } from "@/lib/time"; +import { InsightAnalytics } from "./components/InsightAnalytics"; export default async function Page(props: { params: Promise<{ team_slug: string; project_slug: string; }>; + searchParams: Promise<{ + from?: string | undefined | string[]; + to?: string | undefined | string[]; + interval?: string | undefined | string[]; + }>; }) { - const params = await props.params; + const [params, authToken] = await Promise.all([props.params, getAuthToken()]); + + const project = await getProject(params.team_slug, params.project_slug); - const [team, project] = await Promise.all([ - getTeamBySlug(params.team_slug), - getProject(params.team_slug, params.project_slug), - ]); + if (!authToken) { + loginRedirect(`/team/${params.team_slug}/${params.project_slug}/insight`); + } - if (!team || !project) { - notFound(); + if (!project) { + redirect(`/team/${params.team_slug}`); } - const activeResponse = await isProjectActive({ - projectId: project.id, - teamId: team.id, + const searchParams = await props.searchParams; + const { range, interval } = getFiltersFromSearchParams({ + defaultRange: "last-30", + from: searchParams.from, + interval: searchParams.interval, + to: searchParams.to, }); - const showFTUX = !activeResponse.insight; + const client = getClientThirdwebClient({ + jwt: authToken, + teamId: project.teamId, + }); return ( -
- {/* header */} -
-
-

- Insight -

-

- APIs to retrieve blockchain data from any EVM chain, enrich it with - metadata, and transform it using custom logic -

-
-
- -
+ +
+ -
- {showFTUX ? ( - - ) : ( - - )} -
- -
-
-
- +
+
+
+
+
+

Get Started with Insight

+

+ A cross-chain API for historic blockchain data. +

+
+ + Learn More + + +
-
- ); -} - -function InsightFooter() { - return ( - + ); }