feat: Basic usage dashboard to show run volume (#501)

* New usage dashboard with static data

* Implement org usage dash

* Grab chart data for the last 12 months

* If no org is found just return undefined so a 404 will be shown

* Remove mock data

* Fill in missing months with 0s

---------

Co-authored-by: James Ritchie <james@jamesritchie.co.uk>
This commit is contained in:
Eric Allam
2023-09-22 17:02:53 -07:00
committed by GitHub
parent 284324031c
commit df24cd5b71
4 changed files with 614 additions and 8 deletions
@@ -0,0 +1,200 @@
import { PrismaClient, prisma } from "~/db.server";
export class OrgUsagePresenter {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({ userId, slug }: { userId: string; slug: string }) {
const organization = await this.#prismaClient.organization.findFirst({
where: {
slug,
members: {
some: {
userId,
},
},
},
});
if (!organization) {
return;
}
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
const startOfLastMonth = new Date(new Date().getFullYear(), new Date().getMonth() - 1, 1); // this works for January as well
// Get count of runs since the start of the current month
const runsCount = await this.#prismaClient.jobRun.count({
where: {
organizationId: organization.id,
createdAt: {
gte: new Date(new Date().getFullYear(), new Date().getMonth(), 1),
},
},
});
// Get the count of runs for last month
const runsCountLastMonth = await this.#prismaClient.jobRun.count({
where: {
organizationId: organization.id,
createdAt: {
gte: startOfLastMonth,
lt: startOfMonth,
},
},
});
// Get the count of the runs for the last 6 months, by month. So for example we want the data shape to be:
// [
// { month: "2021-01", count: 10 },
// { month: "2021-02", count: 20 },
// { month: "2021-03", count: 30 },
// { month: "2021-04", count: 40 },
// { month: "2021-05", count: 50 },
// { month: "2021-06", count: 60 },
// ]
// This will be used to generate the chart on the usage page
// Use prisma queryRaw for this since prisma doesn't support grouping by month
const chartDataRaw = await this.#prismaClient.$queryRaw<
{
month: string;
count: number;
}[]
>`SELECT TO_CHAR("createdAt", 'YYYY-MM') as month, COUNT(*) as count FROM "JobRun" WHERE "organizationId" = ${organization.id} AND "createdAt" >= NOW() - INTERVAL '6 months' GROUP BY month ORDER BY month ASC`;
const chartData = chartDataRaw.map((obj) => ({
name: obj.month,
total: Number(obj.count), // Convert BigInt to Number
}));
const totalJobs = await this.#prismaClient.job.count({
where: {
organizationId: organization.id,
internal: false,
},
});
const totalJobsLastMonth = await this.#prismaClient.job.count({
where: {
organizationId: organization.id,
createdAt: {
lt: startOfMonth,
},
deletedAt: null,
internal: false,
},
});
const totalIntegrations = await this.#prismaClient.integration.count({
where: {
organizationId: organization.id,
},
});
const totalIntegrationsLastMonth = await this.#prismaClient.integration.count({
where: {
organizationId: organization.id,
createdAt: {
lt: startOfMonth,
},
},
});
const totalMembers = await this.#prismaClient.orgMember.count({
where: {
organizationId: organization.id,
},
});
const jobs = await this.#prismaClient.job.findMany({
where: {
organizationId: organization.id,
deletedAt: null,
internal: false,
},
select: {
id: true,
slug: true,
_count: {
select: {
runs: {
where: {
createdAt: {
gte: startOfMonth,
},
},
},
},
},
project: {
select: {
id: true,
name: true,
slug: true,
},
},
},
});
return {
id: organization.id,
runsCount,
runsCountLastMonth,
chartData: fillInMissingMonthlyData(chartData, 6),
totalJobs,
totalJobsLastMonth,
totalIntegrations,
totalIntegrationsLastMonth,
totalMembers,
jobs,
};
}
}
// This will fill in missing chart data with zeros
// So for example, if data is [{ name: "2021-01", total: 10 }, { name: "2021-03", total: 30 }] and the totalNumberOfMonths is 6
// And the current month is "2021-04", then this function will return:
// [{ name: "2020-11", total: 0 }, { name: "2020-12", total: 0 }, { name: "2021-01", total: 10 }, { name: "2021-02", total: 0 }, { name: "2021-03", total: 30 }, { name: "2021-04", total: 0 }]
function fillInMissingMonthlyData(
data: Array<{ name: string; total: number }>,
totalNumberOfMonths: number
): Array<{ name: string; total: number }> {
const currentMonth = new Date().toISOString().slice(0, 7);
const startMonth = new Date(
new Date(currentMonth).getFullYear(),
new Date(currentMonth).getMonth() - totalNumberOfMonths,
1
)
.toISOString()
.slice(0, 7);
const months = getMonthsBetween(startMonth, currentMonth);
let completeData = months.map((month) => {
let foundData = data.find((d) => d.name === month);
return foundData ? { ...foundData } : { name: month, total: 0 };
});
return completeData;
}
function getMonthsBetween(startMonth: string, endMonth: string): string[] {
const startDate = new Date(startMonth);
const endDate = new Date(endMonth);
const months = [];
let currentDate = startDate;
while (currentDate <= endDate) {
months.push(currentDate.toISOString().slice(0, 7));
currentDate = new Date(currentDate.setMonth(currentDate.getMonth() + 1));
}
months.push(endMonth);
return months;
}
@@ -1,17 +1,178 @@
import { ComingSoon } from "~/components/ComingSoon";
import { PageContainer, PageBody } from "~/components/layout/AppLayout";
import { ArrowRightIcon } from "@heroicons/react/20/solid";
import {
ForwardIcon,
SquaresPlusIcon,
UsersIcon,
WrenchScrewdriverIcon,
} from "@heroicons/react/24/solid";
import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Header2 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { TextLink } from "~/components/primitives/TextLink";
import { useOrganization } from "~/hooks/useOrganizations";
import { OrganizationParamsSchema, jobPath, organizationTeamPath } from "~/utils/pathBuilder";
import { OrgAdminHeader } from "../_app.orgs.$organizationSlug._index/OrgAdminHeader";
import { Link } from "@remix-run/react/dist/components";
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { OrgUsagePresenter } from "~/presenters/OrgUsagePresenter.server";
import { requireUserId } from "~/services/session.server";
export async function loader({ params, request }: LoaderArgs) {
const userId = await requireUserId(request);
const { organizationSlug } = OrganizationParamsSchema.parse(params);
const presenter = new OrgUsagePresenter();
const data = await presenter.call({ userId, slug: organizationSlug });
if (!data) {
throw new Response(null, { status: 404 });
}
return typedjson(data);
}
const CustomTooltip = ({ active, payload, label }: TooltipProps<number, string>) => {
if (active && payload) {
return (
<div className="flex items-center gap-2 rounded border border-border bg-slate-900 px-4 py-2 text-sm text-dimmed">
<p className="text-white">{label}:</p>
<p className="text-white">{payload[0].value}</p>
</div>
);
}
return null;
};
export default function Page() {
const organization = useOrganization();
const loaderData = useTypedLoaderData<typeof loader>();
return (
<PageContainer>
<OrgAdminHeader />
<PageBody>
<ComingSoon
title="Usage & billing"
description="View your usage, tier and billing information. During the beta we will display usage and start billing if you exceed your limits. But don't worry, we'll give you plenty of warning."
icon="billing"
/>
<div className="mb-4 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header2>Total Runs this month</Header2>
<ForwardIcon className="h-6 w-6 text-dimmed" />
</div>
<div>
<p className="text-3xl font-bold">{loaderData.runsCount.toLocaleString()}</p>
<Paragraph variant="small" className="text-dimmed">
{loaderData.runsCountLastMonth} runs last month
</Paragraph>
</div>
</div>
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header2>Total Jobs</Header2>
<WrenchScrewdriverIcon className="h-6 w-6 text-dimmed" />
</div>
<div>
<p className="text-3xl font-bold">{loaderData.totalJobs.toLocaleString()}</p>
<Paragraph variant="small" className="text-dimmed">
{loaderData.totalJobs === loaderData.totalJobsLastMonth ? (
<>No change since last month</>
) : loaderData.totalJobs > loaderData.totalJobsLastMonth ? (
<>+{loaderData.totalJobs - loaderData.totalJobsLastMonth} since last month</>
) : (
<>-{loaderData.totalJobsLastMonth - loaderData.totalJobs} since last month</>
)}
</Paragraph>
</div>
</div>
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header2>Total Integrations</Header2>
<SquaresPlusIcon className="h-6 w-6 text-dimmed" />
</div>
<div>
<p className="text-3xl font-bold">{loaderData.totalIntegrations.toLocaleString()}</p>
<Paragraph variant="small" className="text-dimmed">
{loaderData.totalIntegrations === loaderData.totalIntegrationsLastMonth ? (
<>No change since last month</>
) : loaderData.totalIntegrations > loaderData.totalIntegrationsLastMonth ? (
<>
+{loaderData.totalIntegrations - loaderData.totalIntegrationsLastMonth} since
last month
</>
) : (
<>
-{loaderData.totalIntegrationsLastMonth - loaderData.totalIntegrations} since
last month
</>
)}
</Paragraph>
</div>
</div>
<div className="rounded border border-border p-6">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Header2>Team members</Header2>
<UsersIcon className="h-6 w-6 text-dimmed" />
</div>
<div>
<p className="text-3xl font-bold">{loaderData.totalMembers.toLocaleString()}</p>
<TextLink
to={organizationTeamPath(organization)}
className="group text-sm text-dimmed hover:text-bright"
>
Manage
<ArrowRightIcon className="-mb-0.5 ml-0.5 h-4 w-4 text-dimmed transition group-hover:translate-x-1 group-hover:text-bright" />
</TextLink>
</div>
</div>
</div>
<div className="flex max-h-[500px] gap-x-4">
<div className="w-1/2 rounded border border-border py-6 pr-2">
<Header2 className="mb-8 pl-6">Job Runs per month</Header2>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={loaderData.chartData}>
<XAxis
dataKey="name"
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<YAxis
stroke="#888888"
fontSize={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `${value}`}
/>
<Tooltip cursor={{ fill: "rgba(255,255,255,0.05)" }} content={<CustomTooltip />} />
<Bar dataKey="total" fill="#DB2777" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
<div className="w-1/2 overflow-y-auto rounded border border-border px-3 py-6">
<div className="mb-2 flex items-baseline justify-between border-b border-border px-3 pb-4">
<Header2 className="">Jobs</Header2>
<Header2 className="">Runs</Header2>
</div>
<div className="space-y-2">
{loaderData.jobs.map((job) => (
<Link
to={jobPath(organization, job.project, job)}
className="flex items-center rounded px-4 py-3 transition hover:bg-slate-850"
key={job.id}
>
<div className="space-y-1">
<p className="text-sm font-medium leading-none">{job.slug}</p>
<p className="text-sm text-muted-foreground">Project: {job.project.name}</p>
</div>
<div className="ml-auto font-medium">{job._count.runs.toLocaleString()}</div>
</Link>
))}
</div>
</div>
</div>
</PageBody>
</PageContainer>
);
+2 -1
View File
@@ -61,8 +61,8 @@
"@remix-run/server-runtime": "1.19.2-pre.0",
"@team-plain/typescript-sdk": "^2.2.0",
"@trigger.dev/companyicons": "^1.5.14",
"@trigger.dev/database": "workspace:*",
"@trigger.dev/core": "workspace:*",
"@trigger.dev/database": "workspace:*",
"@trigger.dev/sdk": "workspace:*",
"@uiw/react-codemirror": "^4.19.5",
"class-variance-authority": "^0.5.2",
@@ -96,6 +96,7 @@
"react-hot-toast": "^2.4.0",
"react-hotkeys-hook": "^3.4.7",
"react-use": "^17.4.0",
"recharts": "^2.8.0",
"remix-auth": "^3.2.2",
"remix-auth-email-link": "^1.4.2",
"remix-auth-github": "^1.1.1",
+244
View File
@@ -171,6 +171,7 @@ importers:
react-hot-toast: ^2.4.0
react-hotkeys-hook: ^3.4.7
react-use: ^17.4.0
recharts: ^2.8.0
remix-auth: ^3.2.2
remix-auth-email-link: ^1.4.2
remix-auth-github: ^1.1.1
@@ -264,6 +265,7 @@ importers:
react-hot-toast: 2.4.0_biqbaboplfbrettd7655fr4n2y
react-hotkeys-hook: 3.4.7_biqbaboplfbrettd7655fr4n2y
react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y
recharts: 2.8.0_v2m5e27vhdewzwhryxwfaorcca
remix-auth: 3.4.0_mrckq3wlqfipa3hs7ezq3k3x3y
remix-auth-email-link: 1.5.2_xmjsiulzsxcc3znmuhq3turs2q
remix-auth-github: 1.3.0_xmjsiulzsxcc3znmuhq3turs2q
@@ -12622,6 +12624,48 @@ packages:
'@types/node': 20.6.0
dev: false
/@types/d3-array/3.0.8:
resolution: {integrity: sha512-2xAVyAUgaXHX9fubjcCbGAUOqYfRJN1em1EKR2HfzWBpObZhwfnZKvofTN4TplMqJdFQao61I+NVSai/vnBvDQ==}
dev: false
/@types/d3-color/3.1.1:
resolution: {integrity: sha512-CSAVrHAtM9wfuLJ2tpvvwCU/F22sm7rMHNN+yh9D6O6hyAms3+O0cgMpC1pm6UEUMOntuZC8bMt74PteiDUdCg==}
dev: false
/@types/d3-ease/3.0.0:
resolution: {integrity: sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==}
dev: false
/@types/d3-interpolate/3.0.2:
resolution: {integrity: sha512-zAbCj9lTqW9J9PlF4FwnvEjXZUy75NQqPm7DMHZXuxCFTpuTrdK2NMYGQekf4hlasL78fCYOLu4EE3/tXElwow==}
dependencies:
'@types/d3-color': 3.1.1
dev: false
/@types/d3-path/3.0.0:
resolution: {integrity: sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==}
dev: false
/@types/d3-scale/4.0.5:
resolution: {integrity: sha512-w/C++3W394MHzcLKO2kdsIn5KKNTOqeQVzyPSGPLzQbkPw/jpeaGtSRlakcKevGgGsjJxGsbqS0fPrVFDbHrDA==}
dependencies:
'@types/d3-time': 3.0.1
dev: false
/@types/d3-shape/3.1.3:
resolution: {integrity: sha512-cHMdIq+rhF5IVwAV7t61pcEXfEHsEsrbBUPkFGBwTXuxtTAkBBrnrNA8++6OWm3jwVsXoZYQM8NEekg6CPJ3zw==}
dependencies:
'@types/d3-path': 3.0.0
dev: false
/@types/d3-time/3.0.1:
resolution: {integrity: sha512-5j/AnefKAhCw4HpITmLDTPlf4vhi8o/dES+zbegfPb7LaGfNyqkLxBR6E+4yvTAgnJLmhe80EXFMzUs38fw4oA==}
dev: false
/@types/d3-timer/3.0.0:
resolution: {integrity: sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==}
dev: false
/@types/debug/4.1.7:
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
dependencies:
@@ -15576,6 +15620,10 @@ packages:
typescript: 4.9.4
dev: false
/classnames/2.3.2:
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
dev: false
/clean-css/5.3.2:
resolution: {integrity: sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==}
engines: {node: '>= 10.0'}
@@ -16144,6 +16192,10 @@ packages:
source-map: 0.6.1
dev: false
/css-unit-converter/1.1.2:
resolution: {integrity: sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==}
dev: false
/css-what/5.1.0:
resolution: {integrity: sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==}
engines: {node: '>= 6'}
@@ -16195,6 +16247,77 @@ packages:
type: 1.2.0
dev: false
/d3-array/3.2.4:
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
engines: {node: '>=12'}
dependencies:
internmap: 2.0.3
dev: false
/d3-color/3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
dev: false
/d3-ease/3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
dev: false
/d3-format/3.1.0:
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
engines: {node: '>=12'}
dev: false
/d3-interpolate/3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
dependencies:
d3-color: 3.1.0
dev: false
/d3-path/3.1.0:
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
engines: {node: '>=12'}
dev: false
/d3-scale/4.0.2:
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
d3-format: 3.1.0
d3-interpolate: 3.0.1
d3-time: 3.1.0
d3-time-format: 4.1.0
dev: false
/d3-shape/3.2.0:
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
engines: {node: '>=12'}
dependencies:
d3-path: 3.1.0
dev: false
/d3-time-format/4.1.0:
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
engines: {node: '>=12'}
dependencies:
d3-time: 3.1.0
dev: false
/d3-time/3.1.0:
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
engines: {node: '>=12'}
dependencies:
d3-array: 3.2.4
dev: false
/d3-timer/3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
dev: false
/damerau-levenshtein/1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -16318,6 +16441,10 @@ packages:
engines: {node: '>=10'}
dev: true
/decimal.js-light/2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
dev: false
/decode-named-character-reference/1.0.2:
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
dependencies:
@@ -16625,6 +16752,12 @@ packages:
utila: 0.4.0
dev: true
/dom-helpers/3.4.0:
resolution: {integrity: sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==}
dependencies:
'@babel/runtime': 7.22.5
dev: false
/dom-serializer/1.4.1:
resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
dependencies:
@@ -18783,6 +18916,11 @@ packages:
/fast-deep-equal/3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
/fast-equals/5.0.1:
resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==}
engines: {node: '>=6.0.0'}
dev: false
/fast-fifo/1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
optional: true
@@ -20420,6 +20558,11 @@ packages:
has: 1.0.3
side-channel: 1.0.4
/internmap/2.0.3:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
dev: false
/interpret/1.4.0:
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
engines: {node: '>= 0.10'}
@@ -25012,6 +25155,10 @@ packages:
cssesc: 3.0.0
util-deprecate: 1.0.2
/postcss-value-parser/3.3.1:
resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==}
dev: false
/postcss-value-parser/4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
@@ -25661,6 +25808,10 @@ packages:
/react-is/18.1.0:
resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==}
/react-lifecycles-compat/3.0.4:
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
dev: false
/react-query/3.39.3_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==}
peerDependencies:
@@ -25725,6 +25876,17 @@ packages:
use-sidecar: 1.1.2_e74vmjybjy5dsfplslbsgtbvvi
dev: false
/react-resize-detector/8.1.0_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==}
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
lodash: 4.17.21
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/react-router-dom/6.14.2_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==}
engines: {node: '>=14'}
@@ -25746,6 +25908,20 @@ packages:
'@remix-run/router': 1.7.2
react: 18.2.0
/react-smooth/2.0.4_v2m5e27vhdewzwhryxwfaorcca:
resolution: {integrity: sha512-OkFsrrMBTvQUwEJthE1KXSOj79z57yvEWeFefeXPib+RmQEI9B1Ub1PgzlzzUyBOvl/TjXt5nF2hmD4NsgAh8A==}
peerDependencies:
prop-types: ^15.6.0
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
fast-equals: 5.0.1
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-transition-group: 2.9.0_biqbaboplfbrettd7655fr4n2y
dev: false
/react-style-singleton/2.2.1_e74vmjybjy5dsfplslbsgtbvvi:
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
@@ -25763,6 +25939,20 @@ packages:
tslib: 2.6.2
dev: false
/react-transition-group/2.9.0_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==}
peerDependencies:
react: '>=15.0.0'
react-dom: '>=15.0.0'
dependencies:
dom-helpers: 3.4.0
loose-envify: 1.4.0
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-lifecycles-compat: 3.0.4
dev: false
/react-universal-interface/0.6.2_react@18.2.0+tslib@2.5.0:
resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==}
peerDependencies:
@@ -25906,6 +26096,34 @@ packages:
tslib: 2.6.2
dev: true
/recharts-scale/0.4.5:
resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
dependencies:
decimal.js-light: 2.5.1
dev: false
/recharts/2.8.0_v2m5e27vhdewzwhryxwfaorcca:
resolution: {integrity: sha512-nciXqQDh3aW8abhwUlA4EBOBusRHLNiKHfpRZiG/yjups1x+auHb2zWPuEcTn/IMiN47vVMMuF8Sr+vcQJtsmw==}
engines: {node: '>=12'}
peerDependencies:
prop-types: ^15.6.0
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
classnames: 2.3.2
eventemitter3: 4.0.7
lodash: 4.17.21
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-is: 16.13.1
react-resize-detector: 8.1.0_biqbaboplfbrettd7655fr4n2y
react-smooth: 2.0.4_v2m5e27vhdewzwhryxwfaorcca
recharts-scale: 0.4.5
reduce-css-calc: 2.1.8
victory-vendor: 36.6.11
dev: false
/rechoir/0.6.2:
resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==}
engines: {node: '>= 0.10'}
@@ -25920,6 +26138,13 @@ packages:
strip-indent: 3.0.0
dev: false
/reduce-css-calc/2.1.8:
resolution: {integrity: sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==}
dependencies:
css-unit-converter: 1.1.2
postcss-value-parser: 3.3.1
dev: false
/regenerate-unicode-properties/10.1.0:
resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==}
engines: {node: '>=4'}
@@ -29409,6 +29634,25 @@ packages:
unist-util-stringify-position: 3.0.2
vfile-message: 3.1.3
/victory-vendor/36.6.11:
resolution: {integrity: sha512-nT8kCiJp8dQh8g991J/R5w5eE2KnO8EAIP0xocWlh9l2okngMWglOPoMZzJvek8Q1KUc4XE/mJxTZnvOB1sTYg==}
dependencies:
'@types/d3-array': 3.0.8
'@types/d3-ease': 3.0.0
'@types/d3-interpolate': 3.0.2
'@types/d3-scale': 4.0.5
'@types/d3-shape': 3.1.3
'@types/d3-time': 3.0.1
'@types/d3-timer': 3.0.0
d3-array: 3.2.4
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-scale: 4.0.2
d3-shape: 3.2.0
d3-time: 3.1.0
d3-timer: 3.0.1
dev: false
/vite-node/0.28.5:
resolution: {integrity: sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==}
engines: {node: '>=v14.16.0'}