chore: improve ssr data loading
Some checks failed
Release Workflow / determine-release-type (push) Has been cancelled
Release Workflow / checks (push) Has been cancelled
Release Workflow / e2e-tests (push) Has been cancelled
Release Workflow / build-images (push) Has been cancelled
Release Workflow / publish-release (push) Has been cancelled

This commit is contained in:
Nicolas Meienberger 2026-02-28 16:29:34 +01:00
parent 0ad4d9ab81
commit f3eb57dbc7
8 changed files with 88 additions and 58 deletions

View file

@ -1,6 +1,6 @@
import { createIsomorphicFn } from "@tanstack/react-start";
import { createServerFn } from "@tanstack/react-start";
import { config } from "~/server/core/config";
export const getOrigin = createIsomorphicFn()
.server(() => config.baseUrl)
.client(() => window.location.origin);
export const getOrigin = createServerFn().handler(() => {
return config.baseUrl;
});

View file

@ -2,6 +2,7 @@ import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
import { Shield, ShieldAlert, Trash2 } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import type { GetOrgMembersResponse } from "~/client/api-client";
import {
getOrgMembersOptions,
removeOrgMemberMutation,
@ -24,13 +25,17 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~
import { useOrganizationContext } from "~/client/hooks/use-org-context";
import { cn } from "~/client/lib/utils";
export function OrgMembersSection() {
type OrgMembersSectionProps = {
initialMembers: GetOrgMembersResponse;
};
export function OrgMembersSection({ initialMembers }: OrgMembersSectionProps) {
const { activeMember } = useOrganizationContext();
const [memberToRemove, setMemberToRemove] = useState<{ id: string; name: string } | null>(null);
const orgMembersQuery = useSuspenseQuery({
...getOrgMembersOptions(),
});
const {
data: { members },
} = useSuspenseQuery({ ...getOrgMembersOptions(), initialData: initialMembers });
const updateRole = useMutation({
...updateMemberRoleMutation(),
@ -53,8 +58,6 @@ export function OrgMembersSection() {
},
});
const members = orgMembersQuery.data.members;
return (
<div className="space-y-4">
<div className="rounded-md border">

View file

@ -32,19 +32,25 @@ import { formatDateWithMonth } from "~/client/lib/datetime";
import { getOrigin } from "~/client/functions/get-origin";
import { authClient } from "~/client/lib/auth-client";
import { cn } from "~/client/lib/utils";
import { useServerFn } from "@tanstack/react-start";
import type { GetSsoSettingsResponse } from "~/client/api-client";
type InvitationRole = "member" | "admin" | "owner";
export function SsoSettingsSection() {
const origin = getOrigin();
type SsoSettingsSectionProps = {
initialSettings: GetSsoSettingsResponse;
};
export function SsoSettingsSection({ initialSettings }: SsoSettingsSectionProps) {
const originQuery = useServerFn(getOrigin);
const { data } = useSuspenseQuery({ queryKey: ["app-origin"], queryFn: originQuery });
const navigate = useNavigate();
const { activeOrganization } = useOrganizationContext();
const [inviteEmail, setInviteEmail] = useState("");
const [inviteRole, setInviteRole] = useState<InvitationRole>("member");
const ssoSettingsQuery = useSuspenseQuery({
...getSsoSettingsOptions(),
});
const ssoSettingsQuery = useSuspenseQuery({ ...getSsoSettingsOptions(), initialData: initialSettings });
const updateProviderAutoLinkingMutation = useMutation({
...updateSsoProviderAutoLinkingMutation(),
@ -195,7 +201,7 @@ export function SsoSettingsSection() {
<Input
type="text"
readOnly
value={`${origin}/api/auth/sso/callback/${provider.providerId}`}
value={`${data}/api/auth/sso/callback/${provider.providerId}`}
className="h-8 max-w-62.5 font-mono text-xs text-muted-foreground"
onClick={(e) => e.currentTarget.select()}
/>

View file

@ -24,12 +24,15 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
import { SsoSettingsSection } from "../components/sso/sso-settings-section";
import { OrgMembersSection } from "../components/org-members-section";
import { useOrganizationContext } from "~/client/hooks/use-org-context";
import type { GetOrgMembersResponse, GetSsoSettingsResponse } from "~/client/api-client";
type Props = {
appContext: AppContext;
initialMembers: GetOrgMembersResponse;
initialSsoSettings: GetSsoSettingsResponse;
};
export function SettingsPage({ appContext }: Props) {
export function SettingsPage({ appContext, initialMembers, initialSsoSettings }: Props) {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
@ -296,7 +299,7 @@ export function SettingsPage({ appContext }: Props) {
<CardDescription className="mt-1.5">Manage organization members and roles</CardDescription>
</div>
<CardContent className="p-6">
<OrgMembersSection />
<OrgMembersSection initialMembers={initialMembers} />
</CardContent>
</Card>
@ -309,7 +312,7 @@ export function SettingsPage({ appContext }: Props) {
<CardDescription className="mt-1.5">Configure OIDC provider settings</CardDescription>
</div>
<CardContent className="p-6">
<SsoSettingsSection />
<SsoSettingsSection initialSettings={initialSsoSettings} />
</CardContent>
</Card>
</TabsContent>

View file

@ -12,6 +12,7 @@ import { VolumeIcon } from "~/client/components/volume-icon";
import { listVolumesOptions } from "~/client/api-client/@tanstack/react-query.gen";
import type { VolumeStatus } from "~/client/lib/types";
import { useNavigate } from "@tanstack/react-router";
import { cn } from "~/client/lib/utils";
const getVolumeStatusVariant = (status: VolumeStatus): "success" | "neutral" | "error" | "warning" => {
const statusMap = {
@ -119,38 +120,35 @@ export function VolumesPage() {
</TableRow>
</TableHeader>
<TableBody>
{hasNoFilteredVolumes ? (
<TableRow>
<TableCell colSpan={4} className="text-center py-12">
<div className="flex flex-col items-center gap-3">
<p className="text-muted-foreground">No volumes match your filters.</p>
<Button onClick={clearFilters} variant="outline" size="sm">
<RotateCcw className="h-4 w-4 mr-2" />
Clear filters
</Button>
</div>
<TableRow className={cn({ hidden: !hasNoFilteredVolumes })}>
<TableCell colSpan={4} className="text-center py-12">
<div className="flex flex-col items-center gap-3">
<p className="text-muted-foreground">No volumes match your filters.</p>
<Button onClick={clearFilters} variant="outline" size="sm">
<RotateCcw className="h-4 w-4 mr-2" />
Clear filters
</Button>
</div>
</TableCell>
</TableRow>
{filteredVolumes.map((volume) => (
<TableRow
key={volume.name}
className="hover:bg-white/2 hover:cursor-pointer transition-colors h-12"
onClick={() => navigate({ to: `/volumes/${volume.shortId}` })}
>
<TableCell className="font-medium font-mono text-strong-accent">{volume.name}</TableCell>
<TableCell className="font-mono text-muted-foreground">
<VolumeIcon backend={volume.type} />
</TableCell>
<TableCell className="text-center font-mono">
<StatusDot
variant={getVolumeStatusVariant(volume.status)}
label={volume.status[0].toUpperCase() + volume.status.slice(1)}
/>
</TableCell>
</TableRow>
) : (
filteredVolumes.map((volume) => (
<TableRow
key={volume.name}
className="hover:bg-white/2 hover:cursor-pointer transition-colors border-l-2 border-r-2 border-transparent hover:border-white/10 h-12"
onClick={() => navigate({ to: `/volumes/${volume.shortId}` })}
>
<TableCell className="font-medium font-mono text-strong-accent">{volume.name}</TableCell>
<TableCell className="font-mono text-muted-foreground">
<VolumeIcon backend={volume.type} />
</TableCell>
<TableCell className="text-center font-mono">
<StatusDot
variant={getVolumeStatusVariant(volume.status)}
label={volume.status[0].toUpperCase() + volume.status.slice(1)}
/>
</TableCell>
</TableRow>
))
)}
))}
</TableBody>
</Table>
</div>

View file

@ -3,7 +3,7 @@ import { type } from "arktype";
import { fetchUser } from "../route";
import type { AppContext } from "~/context";
import { AdminPage } from "~/client/modules/admin/routes/admin-page";
import { getAdminUsersOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { getAdminUsersOptions, getRegistrationStatusOptions } from "~/client/api-client/@tanstack/react-query.gen";
export const Route = createFileRoute("/(dashboard)/admin/")({
validateSearch: type({ tab: "string?" }),
@ -15,7 +15,10 @@ export const Route = createFileRoute("/(dashboard)/admin/")({
throw redirect({ to: "/settings" });
}
await context.queryClient.ensureQueryData(getAdminUsersOptions());
await Promise.all([
context.queryClient.ensureQueryData({ ...getAdminUsersOptions() }),
context.queryClient.ensureQueryData({ ...getRegistrationStatusOptions() }),
]);
return authContext as AppContext;
},

View file

@ -5,6 +5,7 @@ import { Layout } from "~/client/components/layout";
import { SIDEBAR_COOKIE_NAME } from "~/client/components/ui/sidebar";
import { authMiddleware } from "~/middleware/auth";
import { auth } from "~/server/lib/auth";
import { getOrganizationContext } from "~/server/lib/functions/organization-context";
import { authService } from "~/server/modules/auth/auth.service";
export const fetchUser = createServerFn({ method: "GET" }).handler(async () => {
@ -24,8 +25,15 @@ export const Route = createFileRoute("/(dashboard)")({
server: {
middleware: [authMiddleware],
},
loader: async () => {
const authContext = await fetchUser();
loader: async ({ context }) => {
const [authContext] = await Promise.all([
fetchUser(),
context.queryClient.ensureQueryData({
queryKey: ["organization-context"],
queryFn: () => getOrganizationContext(),
}),
]);
return authContext;
},
});

View file

@ -3,13 +3,21 @@ import { fetchUser } from "../route";
import type { AppContext } from "~/context";
import { SettingsPage } from "~/client/modules/settings/routes/settings";
import { type } from "arktype";
import { getOrgMembersOptions, getSsoSettingsOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { getOrigin } from "~/client/functions/get-origin";
export const Route = createFileRoute("/(dashboard)/settings/")({
component: RouteComponent,
validateSearch: type({ tab: "string?" }),
loader: async () => {
const authContext = await fetchUser();
return authContext as AppContext;
loader: async ({ context }) => {
const [authContext, org, members] = await Promise.all([
fetchUser(),
context.queryClient.ensureQueryData({ ...getSsoSettingsOptions() }),
context.queryClient.ensureQueryData({ ...getOrgMembersOptions() }),
context.queryClient.ensureQueryData({ queryKey: ["app-origin"], queryFn: () => getOrigin() }),
]);
return { authContext: authContext as AppContext, org, members };
},
staticData: {
breadcrumb: () => [{ label: "Settings" }],
@ -17,6 +25,7 @@ export const Route = createFileRoute("/(dashboard)/settings/")({
});
function RouteComponent() {
const appContext = Route.useLoaderData();
return <SettingsPage appContext={appContext} />;
const { authContext, org, members } = Route.useLoaderData();
return <SettingsPage appContext={authContext} initialMembers={members} initialSsoSettings={org} />;
}