Compare commits
1 commit
main
...
v0.30.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3eb57dbc7 |
8 changed files with 88 additions and 58 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { createIsomorphicFn } from "@tanstack/react-start";
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
import { config } from "~/server/core/config";
|
import { config } from "~/server/core/config";
|
||||||
|
|
||||||
export const getOrigin = createIsomorphicFn()
|
export const getOrigin = createServerFn().handler(() => {
|
||||||
.server(() => config.baseUrl)
|
return config.baseUrl;
|
||||||
.client(() => window.location.origin);
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { Shield, ShieldAlert, Trash2 } from "lucide-react";
|
import { Shield, ShieldAlert, Trash2 } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import type { GetOrgMembersResponse } from "~/client/api-client";
|
||||||
import {
|
import {
|
||||||
getOrgMembersOptions,
|
getOrgMembersOptions,
|
||||||
removeOrgMemberMutation,
|
removeOrgMemberMutation,
|
||||||
|
|
@ -24,13 +25,17 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~
|
||||||
import { useOrganizationContext } from "~/client/hooks/use-org-context";
|
import { useOrganizationContext } from "~/client/hooks/use-org-context";
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
export function OrgMembersSection() {
|
type OrgMembersSectionProps = {
|
||||||
|
initialMembers: GetOrgMembersResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function OrgMembersSection({ initialMembers }: OrgMembersSectionProps) {
|
||||||
const { activeMember } = useOrganizationContext();
|
const { activeMember } = useOrganizationContext();
|
||||||
const [memberToRemove, setMemberToRemove] = useState<{ id: string; name: string } | null>(null);
|
const [memberToRemove, setMemberToRemove] = useState<{ id: string; name: string } | null>(null);
|
||||||
|
|
||||||
const orgMembersQuery = useSuspenseQuery({
|
const {
|
||||||
...getOrgMembersOptions(),
|
data: { members },
|
||||||
});
|
} = useSuspenseQuery({ ...getOrgMembersOptions(), initialData: initialMembers });
|
||||||
|
|
||||||
const updateRole = useMutation({
|
const updateRole = useMutation({
|
||||||
...updateMemberRoleMutation(),
|
...updateMemberRoleMutation(),
|
||||||
|
|
@ -53,8 +58,6 @@ export function OrgMembersSection() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const members = orgMembersQuery.data.members;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
|
|
|
||||||
|
|
@ -32,19 +32,25 @@ import { formatDateWithMonth } from "~/client/lib/datetime";
|
||||||
import { getOrigin } from "~/client/functions/get-origin";
|
import { getOrigin } from "~/client/functions/get-origin";
|
||||||
import { authClient } from "~/client/lib/auth-client";
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
|
import { useServerFn } from "@tanstack/react-start";
|
||||||
|
import type { GetSsoSettingsResponse } from "~/client/api-client";
|
||||||
|
|
||||||
type InvitationRole = "member" | "admin" | "owner";
|
type InvitationRole = "member" | "admin" | "owner";
|
||||||
|
|
||||||
export function SsoSettingsSection() {
|
type SsoSettingsSectionProps = {
|
||||||
const origin = getOrigin();
|
initialSettings: GetSsoSettingsResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SsoSettingsSection({ initialSettings }: SsoSettingsSectionProps) {
|
||||||
|
const originQuery = useServerFn(getOrigin);
|
||||||
|
const { data } = useSuspenseQuery({ queryKey: ["app-origin"], queryFn: originQuery });
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { activeOrganization } = useOrganizationContext();
|
const { activeOrganization } = useOrganizationContext();
|
||||||
const [inviteEmail, setInviteEmail] = useState("");
|
const [inviteEmail, setInviteEmail] = useState("");
|
||||||
const [inviteRole, setInviteRole] = useState<InvitationRole>("member");
|
const [inviteRole, setInviteRole] = useState<InvitationRole>("member");
|
||||||
|
|
||||||
const ssoSettingsQuery = useSuspenseQuery({
|
const ssoSettingsQuery = useSuspenseQuery({ ...getSsoSettingsOptions(), initialData: initialSettings });
|
||||||
...getSsoSettingsOptions(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateProviderAutoLinkingMutation = useMutation({
|
const updateProviderAutoLinkingMutation = useMutation({
|
||||||
...updateSsoProviderAutoLinkingMutation(),
|
...updateSsoProviderAutoLinkingMutation(),
|
||||||
|
|
@ -195,7 +201,7 @@ export function SsoSettingsSection() {
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
readOnly
|
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"
|
className="h-8 max-w-62.5 font-mono text-xs text-muted-foreground"
|
||||||
onClick={(e) => e.currentTarget.select()}
|
onClick={(e) => e.currentTarget.select()}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,15 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
import { SsoSettingsSection } from "../components/sso/sso-settings-section";
|
import { SsoSettingsSection } from "../components/sso/sso-settings-section";
|
||||||
import { OrgMembersSection } from "../components/org-members-section";
|
import { OrgMembersSection } from "../components/org-members-section";
|
||||||
import { useOrganizationContext } from "~/client/hooks/use-org-context";
|
import { useOrganizationContext } from "~/client/hooks/use-org-context";
|
||||||
|
import type { GetOrgMembersResponse, GetSsoSettingsResponse } from "~/client/api-client";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appContext: AppContext;
|
appContext: AppContext;
|
||||||
|
initialMembers: GetOrgMembersResponse;
|
||||||
|
initialSsoSettings: GetSsoSettingsResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SettingsPage({ appContext }: Props) {
|
export function SettingsPage({ appContext, initialMembers, initialSsoSettings }: Props) {
|
||||||
const [currentPassword, setCurrentPassword] = useState("");
|
const [currentPassword, setCurrentPassword] = useState("");
|
||||||
const [newPassword, setNewPassword] = useState("");
|
const [newPassword, setNewPassword] = useState("");
|
||||||
const [confirmPassword, setConfirmPassword] = 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>
|
<CardDescription className="mt-1.5">Manage organization members and roles</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<OrgMembersSection />
|
<OrgMembersSection initialMembers={initialMembers} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
@ -309,7 +312,7 @@ export function SettingsPage({ appContext }: Props) {
|
||||||
<CardDescription className="mt-1.5">Configure OIDC provider settings</CardDescription>
|
<CardDescription className="mt-1.5">Configure OIDC provider settings</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<SsoSettingsSection />
|
<SsoSettingsSection initialSettings={initialSsoSettings} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { VolumeIcon } from "~/client/components/volume-icon";
|
||||||
import { listVolumesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
import { listVolumesOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import type { VolumeStatus } from "~/client/lib/types";
|
import type { VolumeStatus } from "~/client/lib/types";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
const getVolumeStatusVariant = (status: VolumeStatus): "success" | "neutral" | "error" | "warning" => {
|
const getVolumeStatusVariant = (status: VolumeStatus): "success" | "neutral" | "error" | "warning" => {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
|
|
@ -119,8 +120,7 @@ export function VolumesPage() {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{hasNoFilteredVolumes ? (
|
<TableRow className={cn({ hidden: !hasNoFilteredVolumes })}>
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={4} className="text-center py-12">
|
<TableCell colSpan={4} className="text-center py-12">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<p className="text-muted-foreground">No volumes match your filters.</p>
|
<p className="text-muted-foreground">No volumes match your filters.</p>
|
||||||
|
|
@ -131,11 +131,10 @@ export function VolumesPage() {
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
{filteredVolumes.map((volume) => (
|
||||||
filteredVolumes.map((volume) => (
|
|
||||||
<TableRow
|
<TableRow
|
||||||
key={volume.name}
|
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"
|
className="hover:bg-white/2 hover:cursor-pointer transition-colors h-12"
|
||||||
onClick={() => navigate({ to: `/volumes/${volume.shortId}` })}
|
onClick={() => navigate({ to: `/volumes/${volume.shortId}` })}
|
||||||
>
|
>
|
||||||
<TableCell className="font-medium font-mono text-strong-accent">{volume.name}</TableCell>
|
<TableCell className="font-medium font-mono text-strong-accent">{volume.name}</TableCell>
|
||||||
|
|
@ -149,8 +148,7 @@ export function VolumesPage() {
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
))}
|
||||||
)}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { type } from "arktype";
|
||||||
import { fetchUser } from "../route";
|
import { fetchUser } from "../route";
|
||||||
import type { AppContext } from "~/context";
|
import type { AppContext } from "~/context";
|
||||||
import { AdminPage } from "~/client/modules/admin/routes/admin-page";
|
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/")({
|
export const Route = createFileRoute("/(dashboard)/admin/")({
|
||||||
validateSearch: type({ tab: "string?" }),
|
validateSearch: type({ tab: "string?" }),
|
||||||
|
|
@ -15,7 +15,10 @@ export const Route = createFileRoute("/(dashboard)/admin/")({
|
||||||
throw redirect({ to: "/settings" });
|
throw redirect({ to: "/settings" });
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.queryClient.ensureQueryData(getAdminUsersOptions());
|
await Promise.all([
|
||||||
|
context.queryClient.ensureQueryData({ ...getAdminUsersOptions() }),
|
||||||
|
context.queryClient.ensureQueryData({ ...getRegistrationStatusOptions() }),
|
||||||
|
]);
|
||||||
|
|
||||||
return authContext as AppContext;
|
return authContext as AppContext;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Layout } from "~/client/components/layout";
|
||||||
import { SIDEBAR_COOKIE_NAME } from "~/client/components/ui/sidebar";
|
import { SIDEBAR_COOKIE_NAME } from "~/client/components/ui/sidebar";
|
||||||
import { authMiddleware } from "~/middleware/auth";
|
import { authMiddleware } from "~/middleware/auth";
|
||||||
import { auth } from "~/server/lib/auth";
|
import { auth } from "~/server/lib/auth";
|
||||||
|
import { getOrganizationContext } from "~/server/lib/functions/organization-context";
|
||||||
import { authService } from "~/server/modules/auth/auth.service";
|
import { authService } from "~/server/modules/auth/auth.service";
|
||||||
|
|
||||||
export const fetchUser = createServerFn({ method: "GET" }).handler(async () => {
|
export const fetchUser = createServerFn({ method: "GET" }).handler(async () => {
|
||||||
|
|
@ -24,8 +25,15 @@ export const Route = createFileRoute("/(dashboard)")({
|
||||||
server: {
|
server: {
|
||||||
middleware: [authMiddleware],
|
middleware: [authMiddleware],
|
||||||
},
|
},
|
||||||
loader: async () => {
|
loader: async ({ context }) => {
|
||||||
const authContext = await fetchUser();
|
const [authContext] = await Promise.all([
|
||||||
|
fetchUser(),
|
||||||
|
context.queryClient.ensureQueryData({
|
||||||
|
queryKey: ["organization-context"],
|
||||||
|
queryFn: () => getOrganizationContext(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
return authContext;
|
return authContext;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,21 @@ import { fetchUser } from "../route";
|
||||||
import type { AppContext } from "~/context";
|
import type { AppContext } from "~/context";
|
||||||
import { SettingsPage } from "~/client/modules/settings/routes/settings";
|
import { SettingsPage } from "~/client/modules/settings/routes/settings";
|
||||||
import { type } from "arktype";
|
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/")({
|
export const Route = createFileRoute("/(dashboard)/settings/")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
validateSearch: type({ tab: "string?" }),
|
validateSearch: type({ tab: "string?" }),
|
||||||
loader: async () => {
|
loader: async ({ context }) => {
|
||||||
const authContext = await fetchUser();
|
const [authContext, org, members] = await Promise.all([
|
||||||
return authContext as AppContext;
|
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: {
|
staticData: {
|
||||||
breadcrumb: () => [{ label: "Settings" }],
|
breadcrumb: () => [{ label: "Settings" }],
|
||||||
|
|
@ -17,6 +25,7 @@ export const Route = createFileRoute("/(dashboard)/settings/")({
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const appContext = Route.useLoaderData();
|
const { authContext, org, members } = Route.useLoaderData();
|
||||||
return <SettingsPage appContext={appContext} />;
|
|
||||||
|
return <SettingsPage appContext={authContext} initialMembers={members} initialSsoSettings={org} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue