Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions back-end/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import v1_account from './v1/account.ts'
import v1_event from './v1/event.ts'
import v1_purchase from './v1/purchase.ts'
import v1_misc from './v1/misc.ts'
import v1_stats from './v1/stats.ts'

export const router = new Router()

Expand All @@ -15,6 +16,7 @@ v1_account(router)
v1_event(router)
v1_misc(router)
v1_purchase(router)
v1_stats(router)

router.get('/', (ctx) => {
ctx.response.body = 'OK'
Expand Down
25 changes: 25 additions & 0 deletions back-end/routes/v1/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Router, Status } from 'oak'
import { defineRoute } from './_common.ts'
import {withDBConnection, festivalStats, accountReferralStatus} from '../../utils/db.ts'

export default function register(router: Router) {
defineRoute(router, {
endpoint: '/stats',
method: 'get',
requireAuth: true,
handler: async ({ jwt: { account_id }}) => {
// only these accounts are allowed to see stats
// if ([
// 'allowed-account-id-1',
// ].indexOf(account_id) === -1) {
// return [null, Status.Forbidden]
// }

const stats = await withDBConnection(db => {
return festivalStats(db)
})

return [stats, Status.OK]
}
})
}
4 changes: 3 additions & 1 deletion back-end/types/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ export const PURCHASE_TYPES_BY_TYPE = objectFromEntries(
TABLE_ROWS.purchase_type.map(r => [r.purchase_type_id, r])
)

export type PurchaseType = keyof typeof PURCHASE_TYPES_BY_TYPE
export type PurchaseType = keyof typeof PURCHASE_TYPES_BY_TYPE

export type PurchaseCountMap = {[key in PurchaseType]: number}
8 changes: 7 additions & 1 deletion back-end/types/route-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TABLE_ROWS, Tables } from "./db-types.ts"
import { AttendeeInfo, FullAccountInfo } from "./misc.ts"
import {AttendeeInfo, FullAccountInfo, PurchaseCountMap } from "./misc.ts"

export type Routes = {
'/account': {
Expand Down Expand Up @@ -104,7 +104,13 @@ export type Routes = {
start_date: string,
end_date: string
}
},
'/stats': {
method: 'get',
body: undefined,
response: { accounts: number, purchases: PurchaseCountMap }
}

}

export type EventJson = Omit<Tables['event'], 'start_datetime' | 'end_datetime'> & {
Expand Down
31 changes: 29 additions & 2 deletions back-end/utils/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import {
REFERRAL_MAXES,
} from './constants.ts'
import { TableName, Tables } from '../types/db-types.ts'
import { Maybe } from "../types/misc.ts"
import { _format } from 'https://deno.land/std@0.160.0/path/_util.ts'
import {Maybe, PurchaseCountMap } from "../types/misc.ts"
import { WhereClause, queryTableQuery, insertTableQuery, updateTableQuery, deleteTableQuery } from './db-inner.ts'
import {Purchases} from "../types/route-types.ts";
import {objectEntries} from "./misc.ts";

const url = new URL(env.DB_URL)

Expand Down Expand Up @@ -217,3 +218,29 @@ export async function accountReferralStatus(
allowedToPurchase: true,
}
}

export async function festivalStats(
db: Pick<Transaction, 'queryObject'>,
): Promise<{ accounts: number, purchases: PurchaseCountMap> {
const accountRes = (await db.queryObject<
number
>`
SELECT count(*) as accounts from account where password_hash is not null
`).rows

const purchaseRes: Purchases = (await db.queryObject<
Record<Tables['purchase']['purchase_type_id'], number>
>`
SELECT purchase_type_id, count(*) as purchase_count FROM purchase GROUP BY purchase_type_id;
`).rows

const purchases : PurchaseCountMap = {}
for (const [key, value] of objectEntries(purchaseRes)) {
purchases[value['purchase_type_id']] = Number(value['purchase_count'])
}

return {
accounts: accountRes.length ? Number(accountRes[0].accounts) : 0,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like to avoid using numbers in boolean contexts (where 0 is falsy and >0 is truthy), because it can be a foot-gun

In this case I'd just do accounts: Number(accountRes[0].accounts ?? 0)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i agree with you on not doing truey integers, but i don't think im doing that here. if !accountRes.length, then there are 0 accounts

also im not sure i understand the difference in your two examples

purchases: purchases
}
}
65 changes: 65 additions & 0 deletions front-end/src/components/Stats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* eslint-disable indent */
import React, { } from 'react'
import { observer } from 'mobx-react-lite'
import { useRequest } from '../mobx/hooks'
import Col from './core/Col'
import Store from "../Store.ts";
import {vibefetch} from "../vibefetch.ts";
import LoadingDots from "./core/LoadingDots.tsx";
import {
PURCHASE_TYPES_BY_TYPE,
PurchaseCountMap,
PurchaseType
} from "../../../back-end/types/misc.ts";
import {objectKeys} from "../../../back-end/utils/misc.ts";

export default observer(() => {
const stats = useRequest(async () => {
if (!Store.loggedIn) {
return undefined
}

const { response } = await vibefetch(
Store.jwt,
'/stats',
'get',
undefined
)

return response
})

const purchases = stats.state.result?.purchases ?? ({} as PurchaseCountMap)
const purchaseTable = Object.keys(purchases).length > 0 ? (
<table>
{objectKeys<PurchaseCountMap>(purchases).map((key) => {
return (
<tr key={key}>
<th style={{textAlign: 'left', fontWeight: "300"}}>{PURCHASE_TYPES_BY_TYPE[key].description}</th>
<td style={{fontWeight: "400"}}>{purchases[key]}</td>
</tr>
);
})}
</table>
) : <div></div>

return (
<Col padding={20} pageLevel>
<h1 style={{ fontSize: 24, alignSelf: 'flex-start' }}>
Stats
</h1>

{stats.state.kind === 'loading' ? <LoadingDots size={80} color={"blue"}/> : (
<>
<div style={{marginTop: '20px'}}>
Accounts Created: <span style={{fontWeight: "400"}}>{stats.state.result?.accounts}</span>
</div>
<div style={{marginTop: '20px'}}>
<h3>Purchases</h3>
{purchaseTable}
</div>
</>
)}
</Col>
)
})
2 changes: 1 addition & 1 deletion front-end/src/components/Tickets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export class PurchaseFormState {
}
}

const {needsSleepingBags, needsPillow, needsBusTickets} = this.extraPurchasesForm.fieldValues
const { needsSleepingBags, needsPillow, needsBusTickets } = this.extraPurchasesForm.fieldValues

if (needsSleepingBags) {
purchases.SLEEPING_BAG_VIBECLIPSE_2024 = this.allAttendeeForms.length
Expand Down
2 changes: 1 addition & 1 deletion front-end/src/components/core/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type Props = Omit<CommonFieldProps<boolean>, 'label'> & {
children: React.ReactNode
}

export default observer(({ value, onChange, error, onBlur, children}: Props) => {
export default observer(({ value, onChange, error, onBlur, children }: Props) => {
const handleChange = useCallback(() => onChange(!value), [value, onChange])

return (
Expand Down
2 changes: 1 addition & 1 deletion front-end/src/components/core/MultiView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type Props<TView> = {
currentView: TView,
}

function MultiView<TView>({views, currentView}: Props<TView>) {
function MultiView<TView>({ views, currentView }: Props<TView>) {
return (
<div className='multi-view' style={{
'--view-count': views.length,
Expand Down
2 changes: 1 addition & 1 deletion front-end/src/components/core/RadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type Props<T> = Omit<CommonFieldProps<T>, 'value'> & {
options: readonly { value: T, label: string }[]
}

function RadioGroup<T>({label, value, onChange, disabled, error, onBlur, options}: Props<T>) {
function RadioGroup<T>({ label, value, onChange, disabled, error, onBlur, options }: Props<T>) {

const changeHandlers = useStable(() => (value: T | typeof NULL) => () =>
// @ts-expect-error null requires a workaround
Expand Down
2 changes: 1 addition & 1 deletion front-end/src/components/core/RowSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type Props<TOption extends string|number> = {
options: readonly TOption[],
}

function RowSelect<TOption extends string|number>({ label, disabled, value, onChange, options}: Props<TOption>) {
function RowSelect<TOption extends string|number>({ label, disabled, value, onChange, options }: Props<TOption>) {
const handleChange = useStable(() => createTransformer((option: TOption) => () => onChange(option)))

return (
Expand Down
11 changes: 8 additions & 3 deletions front-end/src/views.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { objectEntries } from '../../back-end/utils/misc'
import Account from './components/Account'
import Tickets from './components/Tickets'
import Stats from './components/Stats'

export const VIEWS = {
Tickets: {
Expand All @@ -22,14 +23,18 @@ export const VIEWS = {
Account: {
icon: 'person',
component: Account
}
},
Stats: {
Copy link
Copy Markdown
Collaborator

@brundonsmith brundonsmith Dec 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to give some thought to how to prevent this from showing up for regular users. I'm thinking for now maybe VIEWS becomes dynamic, and factors in our hardcoded list of admin account IDs?

But there is a bigger discussion to be had around, if budibase won't serve all of our admin needs, should we a) look for an alternate service or b) modify our core systems and schemas to properly handle different access levels for admin users?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea this is my core question. there's a also a third option to use something that's meant for dashboards/reports such as https://github.com/metabase/metabase

(why is everything called ____base???)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

partially i did this to get into the codebase a bit. ty for the comments, will fix what you pointed out

icon: 'info',
component: Stats
},
} as const

export const VIEWS_ARRAY = objectEntries(VIEWS)
.map(([name, {icon, component}]) => ({ name, icon, component } as const))
.map(([name, { icon, component }]) => ({ name, icon, component } as const))

export function isViewName(str: string): str is ViewName {
return Object.keys(VIEWS).includes(str)
}

export type ViewName = keyof typeof VIEWS
export type ViewName = keyof typeof VIEWS