Skip to content
Closed
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
4 changes: 3 additions & 1 deletion messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1450,5 +1450,7 @@
"autoLoginRedirecting": "Redirecting to login...",
"autoLoginError": "Auto Login Error",
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL."
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
"unsavedChangesWarning": "You have unsaved changes. Are you sure you want to leave?",
"discardAllChanges": "Discard All Changes"
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function InvitationsDataTable<TData, TValue>({
<DataTable
columns={columns}
data={data}
persistPageSize="invitations-table"
title={t('invite')}
searchPlaceholder={t('inviteSearch')}
searchColumn="email"
Expand Down
1 change: 1 addition & 0 deletions src/app/[orgId]/settings/access/roles/RolesDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function RolesDataTable<TData, TValue>({
<DataTable
columns={columns}
data={data}
persistPageSize="roles-table"
title={t('roles')}
searchPlaceholder={t('accessRolesSearch')}
searchColumn="name"
Expand Down
1 change: 1 addition & 0 deletions src/app/[orgId]/settings/access/users/UsersDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function UsersDataTable<TData, TValue>({
<DataTable
columns={columns}
data={data}
persistPageSize="users-table"
title={t('users')}
searchPlaceholder={t('accessUsersSearch')}
searchColumn="email"
Expand Down
1 change: 1 addition & 0 deletions src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function OrgApiKeysDataTable<TData, TValue>({
<DataTable
columns={columns}
data={data}
persistPageSize="Org-apikeys-table"
title={t('apiKeys')}
searchPlaceholder={t('searchApiKeys')}
searchColumn="name"
Expand Down
1 change: 1 addition & 0 deletions src/app/[orgId]/settings/clients/ClientsDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function ClientsDataTable<TData, TValue>({
<DataTable
columns={columns}
data={data}
persistPageSize="clients-table"
title="Clients"
searchPlaceholder="Search clients..."
searchColumn="name"
Expand Down
1 change: 1 addition & 0 deletions src/app/[orgId]/settings/domains/DomainsDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function DomainsDataTable<TData, TValue>({
<DataTable
columns={columns}
data={data}
persistPageSize="domains-table"
title={t("domains")}
searchPlaceholder={t("domainsSearch")}
searchColumn="baseDomain"
Expand Down
25 changes: 24 additions & 1 deletion src/app/[orgId]/settings/general/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
import { useUserContext } from "@app/hooks/useUserContext";
import { useTranslations } from "next-intl";
import { build } from "@server/build";
import { useFormWithUnsavedChanges } from "@/hooks/useFormWithUnsavedChanges";
import { UnsavedChangesIndicator } from "@/components/navigation-protection/unsaved-changes-indicator";

// Updated schema to include subnet field
const GeneralFormSchema = z.object({
Expand Down Expand Up @@ -68,6 +70,18 @@ export default function GeneralPage() {
mode: "onChange"
});

const {
hasUnsavedChanges,
handleFormSubmit,
clearPersistence,
} = useFormWithUnsavedChanges({
form,
storageKey: `org-general-settings-${org?.org.orgId}`,
excludeFields: [],
warningMessage: t("unsavedChangesWarning")
});


async function deleteOrg() {
setLoadingDelete(true);
try {
Expand Down Expand Up @@ -131,6 +145,7 @@ export default function GeneralPage() {
title: t("orgUpdated"),
description: t("orgUpdatedDescription")
});
clearPersistence();
router.refresh();
})
.catch((e) => {
Expand Down Expand Up @@ -178,10 +193,18 @@ export default function GeneralPage() {
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>

{hasUnsavedChanges && (
<UnsavedChangesIndicator
hasUnsavedChanges={hasUnsavedChanges}
variant="badge"
/>
)}

<SettingsSectionForm>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
onSubmit={handleFormSubmit(onSubmit)}
className="space-y-4"
id="org-settings-form"
>
Expand Down
63 changes: 60 additions & 3 deletions src/app/[orgId]/settings/resources/ResourcesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,42 @@ type ResourcesTableProps = {
defaultView?: "proxy" | "internal";
};


const STORAGE_KEYS = {
PAGE_SIZE: 'datatable-page-size',
getTablePageSize: (tableId?: string) =>
tableId ? `datatable-${tableId}-page-size` : STORAGE_KEYS.PAGE_SIZE
};

const getStoredPageSize = (tableId?: string, defaultSize = 20): number => {
if (typeof window === 'undefined') return defaultSize;

try {
const key = STORAGE_KEYS.getTablePageSize(tableId);
const stored = localStorage.getItem(key);
if (stored) {
const parsed = parseInt(stored, 10);
if (parsed > 0 && parsed <= 1000) {
return parsed;
}
}
} catch (error) {
console.warn('Failed to read page size from localStorage:', error);
}
return defaultSize;
};

const setStoredPageSize = (pageSize: number, tableId?: string): void => {
if (typeof window === 'undefined') return;

try {
const key = STORAGE_KEYS.getTablePageSize(tableId);
localStorage.setItem(key, pageSize.toString());
} catch (error) {
console.warn('Failed to save page size to localStorage:', error);
}
};

export default function ResourcesTable({
resources,
internalResources,
Expand All @@ -113,6 +149,13 @@ export default function ResourcesTable({

const api = createApiClient({ env });

const [proxyPageSize, setProxyPageSize] = useState<number>(() =>
getStoredPageSize('proxy-resources', 20)
);
const [internalPageSize, setInternalPageSize] = useState<number>(() =>
getStoredPageSize('internal-resources', 20)
);

const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedResource, setSelectedResource] =
useState<ResourceRow | null>();
Expand Down Expand Up @@ -559,7 +602,7 @@ export default function ResourcesTable({
onGlobalFilterChange: setProxyGlobalFilter,
initialState: {
pagination: {
pageSize: 20,
pageSize: proxyPageSize,
pageIndex: 0
}
},
Expand All @@ -582,7 +625,7 @@ export default function ResourcesTable({
onGlobalFilterChange: setInternalGlobalFilter,
initialState: {
pagination: {
pageSize: 20,
pageSize: internalPageSize,
pageIndex: 0
}
},
Expand All @@ -593,6 +636,16 @@ export default function ResourcesTable({
}
});

const handleProxyPageSizeChange = (newPageSize: number) => {
setProxyPageSize(newPageSize);
setStoredPageSize(newPageSize, 'proxy-resources');
};

const handleInternalPageSizeChange = (newPageSize: number) => {
setInternalPageSize(newPageSize);
setStoredPageSize(newPageSize, 'internal-resources');
};

return (
<>
{selectedResource && (
Expand Down Expand Up @@ -761,7 +814,10 @@ export default function ResourcesTable({
</TableBody>
</Table>
<div className="mt-4">
<DataTablePagination table={proxyTable} />
<DataTablePagination
table={proxyTable}
onPageSizeChange={handleProxyPageSizeChange}
/>
</div>
</TabsContent>
<TabsContent value="internal">
Expand Down Expand Up @@ -861,6 +917,7 @@ export default function ResourcesTable({
<div className="mt-4">
<DataTablePagination
table={internalTable}
onPageSizeChange={handleInternalPageSizeChange}
/>
</div>
</TabsContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { ListRolesResponse } from "@server/routers/role";
import { toast } from "@app/hooks/useToast";
import { useOrgContext } from "@app/hooks/useOrgContext";
Expand Down Expand Up @@ -58,6 +58,8 @@ import {
SelectValue
} from "@app/components/ui/select";
import { Separator } from "@app/components/ui/separator";
import { useUnsavedChanges } from "@app/hooks/useUnsavedChanges";
import { UnsavedChangesIndicator } from "@app/components/navigation-protection/unsaved-changes-indicator";

const UsersRolesFormSchema = z.object({
roles: z.array(
Expand Down Expand Up @@ -148,6 +150,48 @@ export default function ResourceAuthenticationPage() {
defaultValues: { emails: [] }
});


const hasLocalUsersRolesChanges = useMemo(() => {
const { roles, users } = usersRolesForm.getValues();
return roles.length > 0 || users.length > 0;
}, [usersRolesForm.watch("roles"), usersRolesForm.watch("users")]);

const hasLocalWhitelistChanges = useMemo(() => {
const { emails } = whitelistForm.getValues();
return emails.length > 0 || whitelistEnabled !== resource.emailWhitelistEnabled;
}, [whitelistForm.watch("emails"), whitelistEnabled, resource.emailWhitelistEnabled]);

const { setIsNavigating: setUsersRolesNavigating } = useUnsavedChanges({
hasUnsavedChanges: hasLocalUsersRolesChanges,
message: t("unsavedChangesWarning")
});

const { setIsNavigating: setWhitelistNavigating } = useUnsavedChanges({
hasUnsavedChanges: hasLocalWhitelistChanges,
message: t("unsavedChangesWarning")
});

const handleDiscardUsersRolesChanges = () => {
setUsersRolesNavigating(true);
usersRolesForm.reset({ roles: [], users: [] });
setTimeout(() => setUsersRolesNavigating(false), 0);
};

const handleDiscardWhitelistChanges = () => {
setWhitelistNavigating(true);
whitelistForm.reset({ emails: [] });
setWhitelistEnabled(resource.emailWhitelistEnabled);
setTimeout(() => setWhitelistNavigating(false), 0);
};

// Aggregate unsaved check
const hasAnyUnsavedChanges = useMemo(() => {
return hasLocalUsersRolesChanges || hasLocalWhitelistChanges;
}, [hasLocalUsersRolesChanges, hasLocalWhitelistChanges]);




useEffect(() => {
const fetchData = async () => {
try {
Expand Down Expand Up @@ -457,6 +501,12 @@ export default function ResourceAuthenticationPage() {
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{hasLocalUsersRolesChanges && (
<UnsavedChangesIndicator
hasUnsavedChanges={hasLocalUsersRolesChanges}
variant="badge"
/>
)}
<SettingsSectionForm>
<SwitchInput
id="sso-toggle"
Expand Down Expand Up @@ -680,14 +730,24 @@ export default function ResourceAuthenticationPage() {
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={loadingSaveUsersRoles}
disabled={loadingSaveUsersRoles}
form="users-roles-form"
>
{t("resourceUsersRolesSubmit")}
</Button>
<div className="flex justify-end gap-4">
<Button
type="submit"
loading={loadingSaveUsersRoles}
disabled={loadingSaveUsersRoles}
form="users-roles-form"
>
{t("resourceUsersRolesSubmit")}
</Button>
{hasAnyUnsavedChanges && (
<Button
variant="outline"
onClick={handleDiscardUsersRolesChanges}
>
{t("discardAllChanges")}
</Button>
)}
</div>
</SettingsSectionFooter>
</SettingsSection>

Expand Down Expand Up @@ -775,6 +835,12 @@ export default function ResourceAuthenticationPage() {
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{hasLocalWhitelistChanges && (
<UnsavedChangesIndicator
hasUnsavedChanges={hasLocalWhitelistChanges}
variant="badge"
/>
)}
<SettingsSectionForm>
{!env.email.emailEnabled && (
<Alert variant="neutral" className="mb-4">
Expand Down Expand Up @@ -885,14 +951,24 @@ export default function ResourceAuthenticationPage() {
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
onClick={saveWhitelist}
form="whitelist-form"
loading={loadingSaveWhitelist}
disabled={loadingSaveWhitelist}
>
{t("otpEmailWhitelistSave")}
</Button>
<div className="flex justify-end gap-4">
<Button
onClick={saveWhitelist}
form="whitelist-form"
loading={loadingSaveWhitelist}
disabled={loadingSaveWhitelist}
>
{t("otpEmailWhitelistSave")}
</Button>
{hasAnyUnsavedChanges && (
<Button
variant="outline"
onClick={handleDiscardWhitelistChanges}
>
{t("discardAllChanges")}
</Button>
)}
</div>
</SettingsSectionFooter>
</SettingsSection>
</SettingsContainer>
Expand Down
Loading