|
1 |
| -import { ComingSoon } from "~/components/ComingSoon"; |
2 |
| -import { PageContainer, PageBody } from "~/components/layout/AppLayout"; |
| 1 | +import { ArrowRightIcon } from "@heroicons/react/20/solid"; |
| 2 | +import { |
| 3 | + ForwardIcon, |
| 4 | + SquaresPlusIcon, |
| 5 | + UsersIcon, |
| 6 | + WrenchScrewdriverIcon, |
| 7 | +} from "@heroicons/react/24/solid"; |
| 8 | +import { Bar, BarChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts"; |
| 9 | +import { PageBody, PageContainer } from "~/components/layout/AppLayout"; |
| 10 | +import { Header2 } from "~/components/primitives/Headers"; |
| 11 | +import { Paragraph } from "~/components/primitives/Paragraph"; |
| 12 | +import { TextLink } from "~/components/primitives/TextLink"; |
| 13 | +import { useOrganization } from "~/hooks/useOrganizations"; |
| 14 | +import { OrganizationParamsSchema, jobPath, organizationTeamPath } from "~/utils/pathBuilder"; |
3 | 15 | import { OrgAdminHeader } from "../_app.orgs.$organizationSlug._index/OrgAdminHeader";
|
| 16 | +import { Link } from "@remix-run/react/dist/components"; |
| 17 | +import { LoaderArgs } from "@remix-run/server-runtime"; |
| 18 | +import { typedjson, useTypedLoaderData } from "remix-typedjson"; |
| 19 | +import { OrgUsagePresenter } from "~/presenters/OrgUsagePresenter.server"; |
| 20 | +import { requireUserId } from "~/services/session.server"; |
| 21 | + |
| 22 | +export async function loader({ params, request }: LoaderArgs) { |
| 23 | + const userId = await requireUserId(request); |
| 24 | + const { organizationSlug } = OrganizationParamsSchema.parse(params); |
| 25 | + |
| 26 | + const presenter = new OrgUsagePresenter(); |
| 27 | + |
| 28 | + const data = await presenter.call({ userId, slug: organizationSlug }); |
| 29 | + |
| 30 | + if (!data) { |
| 31 | + throw new Response(null, { status: 404 }); |
| 32 | + } |
| 33 | + |
| 34 | + return typedjson(data); |
| 35 | +} |
| 36 | + |
| 37 | +const CustomTooltip = ({ active, payload, label }: TooltipProps<number, string>) => { |
| 38 | + if (active && payload) { |
| 39 | + return ( |
| 40 | + <div className="flex items-center gap-2 rounded border border-border bg-slate-900 px-4 py-2 text-sm text-dimmed"> |
| 41 | + <p className="text-white">{label}:</p> |
| 42 | + <p className="text-white">{payload[0].value}</p> |
| 43 | + </div> |
| 44 | + ); |
| 45 | + } |
| 46 | + |
| 47 | + return null; |
| 48 | +}; |
4 | 49 |
|
5 | 50 | export default function Page() {
|
| 51 | + const organization = useOrganization(); |
| 52 | + const loaderData = useTypedLoaderData<typeof loader>(); |
| 53 | + |
6 | 54 | return (
|
7 | 55 | <PageContainer>
|
8 | 56 | <OrgAdminHeader />
|
9 | 57 | <PageBody>
|
10 |
| - <ComingSoon |
11 |
| - title="Usage & billing" |
12 |
| - 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." |
13 |
| - icon="billing" |
14 |
| - /> |
| 58 | + <div className="mb-4 grid gap-4 md:grid-cols-2 lg:grid-cols-4"> |
| 59 | + <div className="rounded border border-border p-6"> |
| 60 | + <div className="flex flex-row items-center justify-between space-y-0 pb-2"> |
| 61 | + <Header2>Total Runs this month</Header2> |
| 62 | + <ForwardIcon className="h-6 w-6 text-dimmed" /> |
| 63 | + </div> |
| 64 | + <div> |
| 65 | + <p className="text-3xl font-bold">{loaderData.runsCount.toLocaleString()}</p> |
| 66 | + <Paragraph variant="small" className="text-dimmed"> |
| 67 | + {loaderData.runsCountLastMonth} runs last month |
| 68 | + </Paragraph> |
| 69 | + </div> |
| 70 | + </div> |
| 71 | + <div className="rounded border border-border p-6"> |
| 72 | + <div className="flex flex-row items-center justify-between space-y-0 pb-2"> |
| 73 | + <Header2>Total Jobs</Header2> |
| 74 | + <WrenchScrewdriverIcon className="h-6 w-6 text-dimmed" /> |
| 75 | + </div> |
| 76 | + <div> |
| 77 | + <p className="text-3xl font-bold">{loaderData.totalJobs.toLocaleString()}</p> |
| 78 | + <Paragraph variant="small" className="text-dimmed"> |
| 79 | + {loaderData.totalJobs === loaderData.totalJobsLastMonth ? ( |
| 80 | + <>No change since last month</> |
| 81 | + ) : loaderData.totalJobs > loaderData.totalJobsLastMonth ? ( |
| 82 | + <>+{loaderData.totalJobs - loaderData.totalJobsLastMonth} since last month</> |
| 83 | + ) : ( |
| 84 | + <>-{loaderData.totalJobsLastMonth - loaderData.totalJobs} since last month</> |
| 85 | + )} |
| 86 | + </Paragraph> |
| 87 | + </div> |
| 88 | + </div> |
| 89 | + <div className="rounded border border-border p-6"> |
| 90 | + <div className="flex flex-row items-center justify-between space-y-0 pb-2"> |
| 91 | + <Header2>Total Integrations</Header2> |
| 92 | + <SquaresPlusIcon className="h-6 w-6 text-dimmed" /> |
| 93 | + </div> |
| 94 | + <div> |
| 95 | + <p className="text-3xl font-bold">{loaderData.totalIntegrations.toLocaleString()}</p> |
| 96 | + <Paragraph variant="small" className="text-dimmed"> |
| 97 | + {loaderData.totalIntegrations === loaderData.totalIntegrationsLastMonth ? ( |
| 98 | + <>No change since last month</> |
| 99 | + ) : loaderData.totalIntegrations > loaderData.totalIntegrationsLastMonth ? ( |
| 100 | + <> |
| 101 | + +{loaderData.totalIntegrations - loaderData.totalIntegrationsLastMonth} since |
| 102 | + last month |
| 103 | + </> |
| 104 | + ) : ( |
| 105 | + <> |
| 106 | + -{loaderData.totalIntegrationsLastMonth - loaderData.totalIntegrations} since |
| 107 | + last month |
| 108 | + </> |
| 109 | + )} |
| 110 | + </Paragraph> |
| 111 | + </div> |
| 112 | + </div> |
| 113 | + <div className="rounded border border-border p-6"> |
| 114 | + <div className="flex flex-row items-center justify-between space-y-0 pb-2"> |
| 115 | + <Header2>Team members</Header2> |
| 116 | + <UsersIcon className="h-6 w-6 text-dimmed" /> |
| 117 | + </div> |
| 118 | + <div> |
| 119 | + <p className="text-3xl font-bold">{loaderData.totalMembers.toLocaleString()}</p> |
| 120 | + <TextLink |
| 121 | + to={organizationTeamPath(organization)} |
| 122 | + className="group text-sm text-dimmed hover:text-bright" |
| 123 | + > |
| 124 | + Manage |
| 125 | + <ArrowRightIcon className="-mb-0.5 ml-0.5 h-4 w-4 text-dimmed transition group-hover:translate-x-1 group-hover:text-bright" /> |
| 126 | + </TextLink> |
| 127 | + </div> |
| 128 | + </div> |
| 129 | + </div> |
| 130 | + <div className="flex max-h-[500px] gap-x-4"> |
| 131 | + <div className="w-1/2 rounded border border-border py-6 pr-2"> |
| 132 | + <Header2 className="mb-8 pl-6">Job Runs per month</Header2> |
| 133 | + <ResponsiveContainer width="100%" height={400}> |
| 134 | + <BarChart data={loaderData.chartData}> |
| 135 | + <XAxis |
| 136 | + dataKey="name" |
| 137 | + stroke="#888888" |
| 138 | + fontSize={12} |
| 139 | + tickLine={false} |
| 140 | + axisLine={false} |
| 141 | + /> |
| 142 | + <YAxis |
| 143 | + stroke="#888888" |
| 144 | + fontSize={12} |
| 145 | + tickLine={false} |
| 146 | + axisLine={false} |
| 147 | + tickFormatter={(value) => `${value}`} |
| 148 | + /> |
| 149 | + <Tooltip cursor={{ fill: "rgba(255,255,255,0.05)" }} content={<CustomTooltip />} /> |
| 150 | + <Bar dataKey="total" fill="#DB2777" radius={[4, 4, 0, 0]} /> |
| 151 | + </BarChart> |
| 152 | + </ResponsiveContainer> |
| 153 | + </div> |
| 154 | + <div className="w-1/2 overflow-y-auto rounded border border-border px-3 py-6"> |
| 155 | + <div className="mb-2 flex items-baseline justify-between border-b border-border px-3 pb-4"> |
| 156 | + <Header2 className="">Jobs</Header2> |
| 157 | + <Header2 className="">Runs</Header2> |
| 158 | + </div> |
| 159 | + <div className="space-y-2"> |
| 160 | + {loaderData.jobs.map((job) => ( |
| 161 | + <Link |
| 162 | + to={jobPath(organization, job.project, job)} |
| 163 | + className="flex items-center rounded px-4 py-3 transition hover:bg-slate-850" |
| 164 | + key={job.id} |
| 165 | + > |
| 166 | + <div className="space-y-1"> |
| 167 | + <p className="text-sm font-medium leading-none">{job.slug}</p> |
| 168 | + <p className="text-sm text-muted-foreground">Project: {job.project.name}</p> |
| 169 | + </div> |
| 170 | + <div className="ml-auto font-medium">{job._count.runs.toLocaleString()}</div> |
| 171 | + </Link> |
| 172 | + ))} |
| 173 | + </div> |
| 174 | + </div> |
| 175 | + </div> |
15 | 176 | </PageBody>
|
16 | 177 | </PageContainer>
|
17 | 178 | );
|
|
0 commit comments