diff --git a/trailbase-core/js/admin/.prettierrc.mjs b/trailbase-core/js/admin/.prettierrc.mjs deleted file mode 100644 index 85ccfb56..00000000 --- a/trailbase-core/js/admin/.prettierrc.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// .prettierrc.mjs -/** @type {import("prettier").Config} */ -export default { - plugins: ['prettier-plugin-astro'], - overrides: [ - { - files: '*.astro', - options: { - parser: 'astro', - }, - }, - ], -}; diff --git a/trailbase-core/js/admin/src/components/SplitView.tsx b/trailbase-core/js/admin/src/components/SplitView.tsx index 3dc5f768..be25a028 100644 --- a/trailbase-core/js/admin/src/components/SplitView.tsx +++ b/trailbase-core/js/admin/src/components/SplitView.tsx @@ -20,21 +20,20 @@ export function createWindowWidth(): Accessor { return width; } -function setSizes(v: number[] | ((prev: number[]) => number[])) { +function setSizes(next: number[]) { const prev = $sizes.get(); - const next: number[] = typeof v === "function" ? v(prev) : v; const width = window.innerWidth; // This is a bit hacky. On destruction Corvu pops panes and removes sizes one by one. // So switching between pages we'd always start with empty sizes. We basically just avoid // shrinking the array. We also make sure the new relative dimension for element[0] is // within range. - if ( - next.length >= prev.length && - next[0] >= minSizePx / width && - next[0] < maxSizePx / width - ) { - return $sizes.set(next); + if (next.length >= prev.length && next.length > 0) { + const min = minSizePx / width; + const max = maxSizePx / width; + const first = Math.min(max, Math.max(min, next[0])); + + return $sizes.set([first, ...next.slice(1)]); } return prev; } @@ -62,7 +61,7 @@ export function SplitView(props: { onSizesChange={setSizes} orientation="horizontal" > - + @@ -83,7 +82,7 @@ export function SplitView(props: { } const minSizePx = 160; -const maxSizePx = 300; +const maxSizePx = 400; function initialSize(): number[] { const width = window.innerWidth; diff --git a/trailbase-core/js/admin/src/components/auth/UserTable.tsx b/trailbase-core/js/admin/src/components/auth/UserTable.tsx index 3a505c42..9883c04a 100644 --- a/trailbase-core/js/admin/src/components/auth/UserTable.tsx +++ b/trailbase-core/js/admin/src/components/auth/UserTable.tsx @@ -31,79 +31,18 @@ import { Checkbox } from "@/components/ui/checkbox"; import { DataTable } from "@/components/Table"; import { Label } from "@/components/ui/label"; import { AddUser } from "@/components/auth/AddUser"; -import { deleteUser, updateUser } from "@/lib/user"; -import type { - UpdateUserRequest, - UserJson, - ListUsersResponse, -} from "@/lib/bindings"; +import { + deleteUser, + updateUser, + fetchUsers, + type FetchUsersArgs, +} from "@/lib/user"; +import type { UpdateUserRequest, UserJson } from "@/lib/bindings"; import { buildTextFormField, buildOptionalTextFormField, } from "@/components/FormFields"; import { SafeSheet, SheetContainer } from "@/components/SafeSheet"; -import { adminFetch } from "@/lib/fetch"; - -type FetchArgs = { - filter: string | undefined; - pageSize: number; - pageIndex: number; - cursors: string[]; -}; - -export async function fetchUsers( - source: FetchArgs, - { value }: { value: ListUsersResponse | undefined }, -): Promise { - const pageIndex = source.pageIndex; - const limit = source.pageSize; - const cursors = source.cursors; - - const filter = source.filter ?? ""; - const filterQuery = filter - .split("AND") - .map((frag) => frag.trim().replaceAll(" ", "")) - .join("&"); - - console.log("QUERY: ", filterQuery); - - const params = new URLSearchParams(filterQuery); - params.set("limit", limit.toString()); - - // Build the next UUIDv7 "cursor" from previous response and update local - // cursor stack. If we're paging forward we add new cursors, otherwise we're - // re-using previously seen cursors for consistency. We reset if we go back - // to the start. - if (pageIndex === 0) { - cursors.length = 0; - } else { - const index = pageIndex - 1; - if (index < cursors.length) { - // Already known page - params.set("cursor", cursors[index]); - } else { - // New page case: use cursor from previous response or fall back to more - // expensive and inconsistent offset-based pagination. - const cursor = value?.cursor; - if (cursor) { - cursors.push(cursor); - params.set("cursor", cursor); - } else { - params.set("offset", `${pageIndex * source.pageSize}`); - } - } - } - - try { - const response = await adminFetch(`/user?${params}`); - return await response.json(); - } catch (err) { - if (value) { - return value; - } - throw err; - } -} const columnHelper = createColumnHelper(); @@ -114,12 +53,15 @@ function buildColumns( return [ { header: "id", - accessorFn: ({ id }) => id, + accessorKey: "id", + }, + { + header: "email", + accessorKey: "email", }, - columnHelper.accessor("email", { header: "email" }) as ColumnDef, { header: "verified", - accessorFn: ({ verified }) => Boolean(verified), + accessorKey: "verified", }, columnHelper.accessor("id", { header: "Admin", @@ -257,7 +199,7 @@ export function UserTable() { }); const cursors: string[] = []; - const buildFetchArgs = (): FetchArgs => ({ + const buildFetchArgs = (): FetchUsersArgs => ({ pageSize: pagination().pageSize, pageIndex: pagination().pageIndex, cursors: cursors, diff --git a/trailbase-core/js/admin/src/lib/user.ts b/trailbase-core/js/admin/src/lib/user.ts index ba39511a..903e849b 100644 --- a/trailbase-core/js/admin/src/lib/user.ts +++ b/trailbase-core/js/admin/src/lib/user.ts @@ -1,4 +1,8 @@ -import type { UpdateUserRequest, CreateUserRequest } from "@/lib/bindings"; +import type { + UpdateUserRequest, + CreateUserRequest, + ListUsersResponse, +} from "@/lib/bindings"; import { adminFetch } from "@/lib/fetch"; export async function createUser(request: CreateUserRequest) { @@ -33,3 +37,64 @@ export async function updateUser(request: UpdateUserRequest) { body: JSON.stringify(request), }); } + +export type FetchUsersArgs = { + filter: string | undefined; + pageSize: number; + pageIndex: number; + cursors: string[]; +}; + +export async function fetchUsers( + source: FetchUsersArgs, + { value }: { value: ListUsersResponse | undefined }, +): Promise { + const pageIndex = source.pageIndex; + const limit = source.pageSize; + const cursors = source.cursors; + + const filter = source.filter ?? ""; + const filterQuery = filter + .split("AND") + .map((frag) => frag.trim().replaceAll(" ", "")) + .join("&"); + + console.log("QUERY: ", filterQuery); + + const params = new URLSearchParams(filterQuery); + params.set("limit", limit.toString()); + + // Build the next UUIDv7 "cursor" from previous response and update local + // cursor stack. If we're paging forward we add new cursors, otherwise we're + // re-using previously seen cursors for consistency. We reset if we go back + // to the start. + if (pageIndex === 0) { + cursors.length = 0; + } else { + const index = pageIndex - 1; + if (index < cursors.length) { + // Already known page + params.set("cursor", cursors[index]); + } else { + // New page case: use cursor from previous response or fall back to more + // expensive and inconsistent offset-based pagination. + const cursor = value?.cursor; + if (cursor) { + cursors.push(cursor); + params.set("cursor", cursor); + } else { + params.set("offset", `${pageIndex * source.pageSize}`); + } + } + } + + try { + const response = await adminFetch(`/user?${params}`); + return await response.json(); + } catch (err) { + if (value) { + return value; + } + throw err; + } +} diff --git a/trailbase-core/js/client/src/index.ts b/trailbase-core/js/client/src/index.ts index 9a0e529b..6184a8fc 100644 --- a/trailbase-core/js/client/src/index.ts +++ b/trailbase-core/js/client/src/index.ts @@ -271,9 +271,9 @@ class ThinClient { } const response = await fetch(`${this.site}/${path}`, { - ...init, credentials: isDev ? "include" : "same-origin", headers: tokenState.headers, + ...init, }); return response; @@ -439,6 +439,8 @@ export class Client { public async refreshAuthToken(): Promise { const refreshToken = Client.shouldRefresh(this._tokenState); if (refreshToken) { + // TODO: Unset token state if refresh fails with unauthorized status. + // TODO: In either case we should call the authChange, e.g. so that users can persist the new token. this._tokenState = await this.refreshTokensImpl(refreshToken); } } @@ -516,14 +518,11 @@ function _isDev(): boolean { const isDev = _isDev(); export function headers(tokens?: Tokens): HeadersInit { - const base = { - "Content-Type": "application/json", - }; - if (tokens) { const { auth_token, refresh_token, csrf_token } = tokens; return { - ...base, + "Content-Type": "application/json", + ...(auth_token && { Authorization: `Bearer ${auth_token}`, }), @@ -536,7 +535,9 @@ export function headers(tokens?: Tokens): HeadersInit { }; } - return base; + return { + "Content-Type": "application/json", + }; } export function textEncode(s: string): Uint8Array {