feat: add dashboard statistics
All checks were successful
Build and Push Docker Image / tests (push) Successful in 46s
Build and Push Docker Image / build (push) Successful in 4m4s

This commit is contained in:
Lars Hampe 2024-10-31 14:58:06 +01:00
parent af2d0ec4aa
commit 81baf571f9
10 changed files with 256 additions and 9 deletions

View File

@ -9,6 +9,7 @@ import user from './user'
import { accessTokenApi } from './access-token'
import pageApi from './page'
import statisticApi from './statistic'
import { authentication } from './utils/authentication'
import { handleError, handleZodError } from './utils/errors'
import { startup } from './utils/startup'
@ -37,6 +38,7 @@ app.route('/v1/user', user)
app.route('/v1/changelog', changelog)
app.route('/v1/page', pageApi)
app.route('/v1/access-token', accessTokenApi)
app.route('/v1/statistic', statisticApi)
app.doc('/openapi.json', {
openapi: '3.0.0',

View File

@ -0,0 +1,113 @@
import { changelog, db, page } from '@boring.tools/database'
import { StatisticOutput } from '@boring.tools/schema'
import { createRoute } from '@hono/zod-openapi'
import { eq } from 'drizzle-orm'
import type { statisticApi } from '.'
import { verifyAuthentication } from '../utils/authentication'
const route = createRoute({
method: 'get',
path: '/',
tags: ['statistic'],
responses: {
200: {
content: {
'application/json': { schema: StatisticOutput },
},
description: 'Return user',
},
400: {
description: 'Bad Request',
},
500: {
description: 'Internal Server Error',
},
},
})
export const registerStatisticGet = (api: typeof statisticApi) => {
return api.openapi(route, async (c) => {
const userId = verifyAuthentication(c)
const pageResult = await db.query.page.findMany({
where: eq(page.userId, userId),
})
const result = await db.query.changelog.findMany({
where: eq(changelog.userId, userId),
with: {
commits: {
columns: {
id: true,
versionId: true,
},
},
versions: {
columns: {
id: true,
status: true,
},
},
},
})
const changelog_total = result.length
const version_total = result.reduce(
(acc, log) => acc + log.versions.length,
0,
)
const version_published = result.reduce(
(acc, log) =>
acc +
log.versions.filter((version) => version.status === 'published').length,
0,
)
const version_review = result.reduce(
(acc, log) =>
acc +
log.versions.filter((version) => version.status === 'review').length,
0,
)
const version_draft = result.reduce(
(acc, log) =>
acc +
log.versions.filter((version) => version.status === 'draft').length,
0,
)
const commit_total = result.reduce(
(acc, log) => acc + log.commits.length,
0,
)
const commit_unassigned = result.reduce(
(acc, log) =>
acc + log.commits.filter((commit) => !commit.versionId).length,
0,
)
const commit_assigned = result.reduce(
(acc, log) =>
acc + log.commits.filter((commit) => commit.versionId).length,
0,
)
const mappedData = {
changelog: {
total: changelog_total,
versions: {
total: version_total,
published: version_published,
review: version_review,
draft: version_draft,
},
commits: {
total: commit_total,
unassigned: commit_unassigned,
assigned: commit_assigned,
},
},
page: {
total: pageResult.length,
},
}
return c.json(mappedData, 200)
})
}

View File

@ -0,0 +1,14 @@
import { OpenAPIHono } from '@hono/zod-openapi'
import type { Variables } from '..'
import type { ContextModule } from '../utils/sentry'
import { registerStatisticGet } from './get'
export const statisticApi = new OpenAPIHono<{ Variables: Variables }>()
const module: ContextModule = {
name: 'statistic',
}
registerStatisticGet(statisticApi)
export default statisticApi

View File

@ -0,0 +1,20 @@
import type { StatisticOutput } from '@boring.tools/schema'
import { useAuth } from '@clerk/clerk-react'
import { useQuery } from '@tanstack/react-query'
import type { z } from 'zod'
import { queryFetch } from '../utils/queryFetch'
type Statistic = z.infer<typeof StatisticOutput>
export const useStatistic = () => {
const { getToken } = useAuth()
return useQuery({
queryKey: ['statistic'],
queryFn: async (): Promise<Readonly<Statistic>> =>
await queryFetch({
path: 'statistic',
method: 'get',
token: await getToken(),
}),
})
}

View File

@ -1,13 +1,71 @@
import { Card, CardContent, CardHeader, CardTitle } from '@boring.tools/ui'
import { useUser } from '@clerk/clerk-react'
import { createLazyFileRoute } from '@tanstack/react-router'
import { PageWrapper } from '../components/PageWrapper'
import { useStatistic } from '../hooks/useStatistic'
export const Route = createLazyFileRoute('/')({
component: Index,
})
function Index() {
const Component = () => {
const { data } = useStatistic()
const user = useUser()
return (
<div className="p-2">
<h3>Welcome Home!</h3>
</div>
<PageWrapper
breadcrumbs={[
{
name: 'Dashboard',
to: '/',
},
]}
>
<h1 className="text-3xl">Welcome back, {user.user?.fullName}</h1>
<div className="grid w-full max-w-screen-md gap-10 grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Changelogs</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold">
{data?.changelog.total}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Versions</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold">
{data?.changelog.versions.total}
<div className="text-xs text-muted-foreground tracking-normal flex gap-3">
<span>{data?.changelog.versions.published} Published</span>
<span>{data?.changelog.versions.review} Review</span>
<span>{data?.changelog.versions.draft} Draft</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Commits</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold">
{data?.changelog.commits.total}
<div className="text-xs text-muted-foreground tracking-normal flex gap-3">
<span>{data?.changelog.commits.assigned} Assigned</span>
<span>{data?.changelog.commits.unassigned} Unassigned</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Pages</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold">
{data?.page.total}
</CardContent>
</Card>
</div>
</PageWrapper>
)
}
export const Route = createLazyFileRoute('/')({
component: Component,
})

View File

@ -1 +1 @@
{"root":["./src/main.tsx","./src/routeTree.gen.ts","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/Navigation.tsx","./src/components/NavigationMobile.tsx","./src/components/PageWrapper.tsx","./src/components/Sidebar.tsx","./src/components/SidebarChangelog.tsx","./src/components/SidebarPage.tsx","./src/components/SidebarUser.tsx","./src/components/Changelog/CommitList.tsx","./src/components/Changelog/Delete.tsx","./src/components/Changelog/VersionDelete.tsx","./src/components/Changelog/VersionList.tsx","./src/components/Changelog/VersionStatus.tsx","./src/components/Changelog/Version/Create/Step01.tsx","./src/components/Changelog/Version/Create/index.tsx","./src/components/Page/Delete.tsx","./src/hooks/useChangelog.ts","./src/hooks/usePage.ts","./src/routes/__root.tsx","./src/routes/changelog.$id.edit.lazy.tsx","./src/routes/changelog.$id.index.lazy.tsx","./src/routes/changelog.$id.lazy.tsx","./src/routes/changelog.$id.version.$versionId.tsx","./src/routes/changelog.$id.versionCreate.lazy.tsx","./src/routes/changelog.create.lazy.tsx","./src/routes/changelog.index.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/page.$id.edit.lazy.tsx","./src/routes/page.$id.index.lazy.tsx","./src/routes/page.$id.lazy.tsx","./src/routes/page.create.lazy.tsx","./src/routes/page.index.lazy.tsx","./src/routes/user/index.lazy.tsx","./src/utils/navigation-routes.ts","./src/utils/queryFetch.ts"],"version":"5.6.3"}
{"root":["./src/main.tsx","./src/routeTree.gen.ts","./src/vite-env.d.ts","./src/components/Layout.tsx","./src/components/PageWrapper.tsx","./src/components/Sidebar.tsx","./src/components/SidebarChangelog.tsx","./src/components/SidebarPage.tsx","./src/components/SidebarUser.tsx","./src/components/AccessToken/Delete.tsx","./src/components/AccessToken/Table/Columns.tsx","./src/components/AccessToken/Table/DataTable.tsx","./src/components/Changelog/CommitList.tsx","./src/components/Changelog/Delete.tsx","./src/components/Changelog/VersionDelete.tsx","./src/components/Changelog/VersionList.tsx","./src/components/Changelog/VersionStatus.tsx","./src/components/Changelog/Version/Create/Step01.tsx","./src/components/Changelog/Version/Create/index.tsx","./src/components/Page/Delete.tsx","./src/hooks/useAccessToken.ts","./src/hooks/useChangelog.ts","./src/hooks/usePage.ts","./src/routes/__root.tsx","./src/routes/access-tokens.index.lazy.tsx","./src/routes/access-tokens.new.lazy.tsx","./src/routes/changelog.$id.edit.lazy.tsx","./src/routes/changelog.$id.index.lazy.tsx","./src/routes/changelog.$id.lazy.tsx","./src/routes/changelog.$id.version.$versionId.tsx","./src/routes/changelog.$id.versionCreate.lazy.tsx","./src/routes/changelog.create.lazy.tsx","./src/routes/changelog.index.lazy.tsx","./src/routes/cli.lazy.tsx","./src/routes/index.lazy.tsx","./src/routes/page.$id.edit.lazy.tsx","./src/routes/page.$id.index.lazy.tsx","./src/routes/page.$id.lazy.tsx","./src/routes/page.create.lazy.tsx","./src/routes/page.index.lazy.tsx","./src/routes/user/index.lazy.tsx","./src/utils/navigation-routes.ts","./src/utils/queryFetch.ts"],"version":"5.6.3"}

15
bruno/Statistic.bru Normal file
View File

@ -0,0 +1,15 @@
meta {
name: Statistic
type: http
seq: 2
}
get {
url: {{API_URL}}/v1/statistic
body: none
auth: bearer
}
auth:bearer {
token: bt_7b83481e2ae0f6ab728730511adbe491f6668eb3
}

View File

@ -5,3 +5,4 @@ export * from './version'
export * from './page'
export * from './commit'
export * from './access-token'
export * from './statistic'

View File

@ -0,0 +1,23 @@
import { z } from '@hono/zod-openapi'
export const StatisticOutput = z
.object({
changelog: z.object({
total: z.number(),
commits: z.object({
total: z.number(),
unassigned: z.number(),
assigned: z.number(),
}),
versions: z.object({
total: z.number(),
published: z.number(),
review: z.number(),
draft: z.number(),
}),
}),
page: z.object({
total: z.number(),
}),
})
.openapi('Statistic')

View File

@ -0,0 +1 @@
export * from './base'