diff --git a/apps/webapp/app/presenters/OrgUsagePresenter.server.ts b/apps/webapp/app/presenters/OrgUsagePresenter.server.ts new file mode 100644 index 0000000000..d742ea75b4 --- /dev/null +++ b/apps/webapp/app/presenters/OrgUsagePresenter.server.ts @@ -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; +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx index 64c43ecfe5..e0ac76fe11 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.billing/route.tsx @@ -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) => { + if (active && payload) { + return ( +
+

{label}:

+

{payload[0].value}

+
+ ); + } + + return null; +}; export default function Page() { + const organization = useOrganization(); + const loaderData = useTypedLoaderData(); + return ( - +
+
+
+ Total Runs this month + +
+
+

{loaderData.runsCount.toLocaleString()}

+ + {loaderData.runsCountLastMonth} runs last month + +
+
+
+
+ Total Jobs + +
+
+

{loaderData.totalJobs.toLocaleString()}

+ + {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 + )} + +
+
+
+
+ Total Integrations + +
+
+

{loaderData.totalIntegrations.toLocaleString()}

+ + {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 + + )} + +
+
+
+
+ Team members + +
+
+

{loaderData.totalMembers.toLocaleString()}

+ + Manage + + +
+
+
+
+
+ Job Runs per month + + + + `${value}`} + /> + } /> + + + +
+
+
+ Jobs + Runs +
+
+ {loaderData.jobs.map((job) => ( + +
+

{job.slug}

+

Project: {job.project.name}

+
+
{job._count.runs.toLocaleString()}
+ + ))} +
+
+
); diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 427e378ce2..f87202a507 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26a98bf074..e3d62d60dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 @@ -12624,6 +12626,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: @@ -15578,6 +15622,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'} @@ -16146,6 +16194,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'} @@ -16197,6 +16249,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==} @@ -16320,6 +16443,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: @@ -16627,6 +16754,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: @@ -18785,6 +18918,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 @@ -20422,6 +20560,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'} @@ -25014,6 +25157,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==} @@ -25663,6 +25810,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: @@ -25727,6 +25878,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'} @@ -25748,6 +25910,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'} @@ -25765,6 +25941,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: @@ -25908,6 +26098,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'} @@ -25922,6 +26140,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'} @@ -29411,6 +29636,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'}