clean up ui pass 1

This commit is contained in:
miloschwartz
2025-06-30 09:33:48 -07:00
parent 3b6a44e683
commit a0381eb2c6
82 changed files with 17618 additions and 17258 deletions

View File

@@ -8,7 +8,6 @@ import { AxiosResponse } from "axios";
import { authCookieHeader } from "@app/lib/api/cookies";
import { redirect } from "next/navigation";
import { Layout } from "@app/components/Layout";
import { orgLangingNavItems, orgNavItems, rootNavItems } from "../navigation";
import { ListUserOrgsResponse } from "@server/routers/org";
type OrgPageProps = {
@@ -60,7 +59,7 @@ export default async function OrgPage(props: OrgPageProps) {
return (
<UserProvider user={user}>
<Layout orgId={orgId} navItems={orgLangingNavItems} orgs={orgs}>
<Layout orgId={orgId} navItems={[]} orgs={orgs}>
{overview && (
<div className="w-full max-w-4xl mx-auto md:mt-32 mt-4">
<OrganizationLandingCard

View File

@@ -18,6 +18,7 @@ import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl";
import moment from "moment";
export type InvitationRow = {
id: string;
@@ -46,63 +47,69 @@ export default function InvitationsTable({
const { org } = useOrgContext();
const columns: ColumnDef<InvitationRow>[] = [
{
id: "dots",
cell: ({ row }) => {
const invitation = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setIsRegenerateModalOpen(true);
setSelectedInvitation(invitation);
}}
>
<span>{t('inviteRegenerate')}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(true);
setSelectedInvitation(invitation);
}}
>
<span className="text-red-500">
{t('inviteRemove')}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
},
{
accessorKey: "email",
header: t('email')
header: t("email")
},
{
accessorKey: "expiresAt",
header: t('expiresAt'),
header: t("expiresAt"),
cell: ({ row }) => {
const expiresAt = new Date(row.original.expiresAt);
const isExpired = expiresAt < new Date();
return (
<span className={isExpired ? "text-red-500" : ""}>
{expiresAt.toLocaleString()}
{moment(expiresAt).format("lll")}
</span>
);
}
},
{
accessorKey: "role",
header: t('role')
header: t("role")
},
{
id: "dots",
cell: ({ row }) => {
const invitation = row.original;
return (
<div className="flex items-center justify-end gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(true);
setSelectedInvitation(invitation);
}}
>
<span className="text-red-500">
{t("inviteRemove")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
variant={"secondary"}
onClick={() => {
setIsRegenerateModalOpen(true);
setSelectedInvitation(invitation);
}}
>
<span>{t("inviteRegenerate")}</span>
</Button>
</div>
);
}
}
];
@@ -115,16 +122,18 @@ export default function InvitationsTable({
.catch((e) => {
toast({
variant: "destructive",
title: t('inviteRemoveError'),
description: t('inviteRemoveErrorDescription')
title: t("inviteRemoveError"),
description: t("inviteRemoveErrorDescription")
});
});
if (res && res.status === 200) {
toast({
variant: "default",
title: t('inviteRemoved'),
description: t('inviteRemovedDescription', {email: selectedInvitation.email})
title: t("inviteRemoved"),
description: t("inviteRemovedDescription", {
email: selectedInvitation.email
})
});
setInvitations((prev) =>
@@ -148,20 +157,18 @@ export default function InvitationsTable({
dialog={
<div className="space-y-4">
<p>
{t('inviteQuestionRemove', {email: selectedInvitation?.email || ""})}
</p>
<p>
{t('inviteMessageRemove')}
</p>
<p>
{t('inviteMessageConfirm')}
{t("inviteQuestionRemove", {
email: selectedInvitation?.email || ""
})}
</p>
<p>{t("inviteMessageRemove")}</p>
<p>{t("inviteMessageConfirm")}</p>
</div>
}
buttonText={t('inviteRemoveConfirm')}
buttonText={t("inviteRemoveConfirm")}
onConfirm={removeInvitation}
string={selectedInvitation?.email ?? ""}
title={t('inviteRemove')}
title={t("inviteRemove")}
/>
<RegenerateInvitationForm
open={isRegenerateModalOpen}

View File

@@ -201,7 +201,7 @@ export default function RegenerateInvitationForm({
setValidHours(parseInt(value))
}
>
<SelectTrigger>
<SelectTrigger className="w-full">
<SelectValue placeholder={t('inviteValidityPeriodSelect')} />
</SelectTrigger>
<SelectContent>

View File

@@ -159,7 +159,6 @@ export default function DeleteRoleForm({
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="space-y-4">
<div className="space-y-4">
<p>
{t('accessRoleQuestionRemove', {name: roleToDelete.name})}
@@ -210,13 +209,13 @@ export default function DeleteRoleForm({
/>
</form>
</Form>
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t('close')}</Button>
</CredenzaClose>
<Button
variant="destructive"
type="submit"
form="remove-role-form"
loading={loading}

View File

@@ -19,7 +19,7 @@ import CreateRoleForm from "./CreateRoleForm";
import DeleteRoleForm from "./DeleteRoleForm";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useTranslations } from 'next-intl';
import { useTranslations } from "next-intl";
export type RoleRow = Role;
@@ -42,49 +42,6 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
const t = useTranslations();
const columns: ColumnDef<RoleRow>[] = [
{
id: "actions",
cell: ({ row }) => {
const roleRow = row.original;
return (
<>
<div>
{roleRow.isAdmin && (
<MoreHorizontal className="h-4 w-4 opacity-0" />
)}
{!roleRow.isAdmin && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
{t('openMenu')}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(true);
setUserToRemove(roleRow);
}}
>
<span className="text-red-500">
{t('accessRoleDelete')}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</>
);
}
},
{
accessorKey: "name",
header: ({ column }) => {
@@ -95,7 +52,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -103,7 +60,29 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
},
{
accessorKey: "description",
header: t('description')
header: t("description")
},
{
id: "actions",
cell: ({ row }) => {
const roleRow = row.original;
return (
<div className="flex items-center justify-end">
<Button
variant={"secondary"}
size="sm"
disabled={roleRow.isAdmin || false}
onClick={() => {
setIsDeleteModalOpen(true);
setUserToRemove(roleRow);
}}
>
{t("accessRoleDelete")}
</Button>
</div>
);
}
}
];

View File

@@ -6,8 +6,6 @@ import { cache } from "react";
import OrgProvider from "@app/providers/OrgProvider";
import { ListRolesResponse } from "@server/routers/role";
import RolesTable, { RoleRow } from "./RolesTable";
import { SidebarSettings } from "@app/components/SidebarSettings";
import AccessPageHeaderAndNav from "../AccessPageHeaderAndNav";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { getTranslations } from 'next-intl/server';

View File

@@ -20,7 +20,7 @@ import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useUserContext } from "@app/hooks/useUserContext";
import { useTranslations } from 'next-intl';
import { useTranslations } from "next-intl";
export type UserRow = {
id: string;
@@ -51,65 +51,6 @@ export default function UsersTable({ users: u }: UsersTableProps) {
const t = useTranslations();
const columns: ColumnDef<UserRow>[] = [
{
id: "dots",
cell: ({ row }) => {
const userRow = row.original;
return (
<>
<div>
{userRow.isOwner && (
<MoreHorizontal className="h-4 w-4 opacity-0" />
)}
{!userRow.isOwner && (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
{t('openMenu')}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
className="block w-full"
>
<DropdownMenuItem>
{t('accessUsersManage')}
</DropdownMenuItem>
</Link>
{`${userRow.username}-${userRow.idpId}` !==
`${user?.username}-${userRow.idpId}` && (
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(
true
);
setSelectedUser(
userRow
);
}}
>
<span className="text-red-500">
{t('accessUserRemove')}
</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
</>
);
}
},
{
accessorKey: "displayUsername",
header: ({ column }) => {
@@ -120,7 +61,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('username')}
{t("username")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -136,7 +77,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('identityProvider')}
{t("identityProvider")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -152,7 +93,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('role')}
{t("role")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -176,12 +117,68 @@ export default function UsersTable({ users: u }: UsersTableProps) {
const userRow = row.original;
return (
<div className="flex items-center justify-end">
<>
<div>
{userRow.isOwner && (
<MoreHorizontal className="h-4 w-4 opacity-0" />
)}
{!userRow.isOwner && (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
className="block w-full"
>
<DropdownMenuItem>
{t("accessUsersManage")}
</DropdownMenuItem>
</Link>
{`${userRow.username}-${userRow.idpId}` !==
`${user?.username}-${userRow.idpId}` && (
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(
true
);
setSelectedUser(
userRow
);
}}
>
<span className="text-red-500">
{t(
"accessUserRemove"
)}
</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
</>
{userRow.isOwner && (
<Button
variant="ghost"
className="opacity-0 cursor-default"
variant={"secondary"}
className="ml-2"
size="sm"
disabled={true}
>
{t('placeholder')}
{t("manage")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
)}
{!userRow.isOwner && (
@@ -189,10 +186,12 @@ export default function UsersTable({ users: u }: UsersTableProps) {
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
>
<Button
variant={"outlinePrimary"}
variant={"secondary"}
className="ml-2"
size="sm"
disabled={userRow.isOwner}
>
{t('manage')}
{t("manage")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@@ -210,10 +209,10 @@ export default function UsersTable({ users: u }: UsersTableProps) {
.catch((e) => {
toast({
variant: "destructive",
title: t('userErrorOrgRemove'),
title: t("userErrorOrgRemove"),
description: formatAxiosError(
e,
t('userErrorOrgRemoveDescription')
t("userErrorOrgRemoveDescription")
)
});
});
@@ -221,8 +220,10 @@ export default function UsersTable({ users: u }: UsersTableProps) {
if (res && res.status === 200) {
toast({
variant: "default",
title: t('userOrgRemoved'),
description: t('userOrgRemovedDescription', {email: selectedUser.email || ""})
title: t("userOrgRemoved"),
description: t("userOrgRemovedDescription", {
email: selectedUser.email || ""
})
});
setUsers((prev) =>
@@ -244,19 +245,21 @@ export default function UsersTable({ users: u }: UsersTableProps) {
dialog={
<div className="space-y-4">
<p>
{t('userQuestionOrgRemove', {email: selectedUser?.email || selectedUser?.name || selectedUser?.username || ""})}
{t("userQuestionOrgRemove", {
email:
selectedUser?.email ||
selectedUser?.name ||
selectedUser?.username ||
""
})}
</p>
<p>
{t('userMessageOrgRemove')}
</p>
<p>{t("userMessageOrgRemove")}</p>
<p>
{t('userMessageOrgConfirm')}
</p>
<p>{t("userMessageOrgConfirm")}</p>
</div>
}
buttonText={t('userRemoveOrgConfirm')}
buttonText={t("userRemoveOrgConfirm")}
onConfirm={removeUser}
string={
selectedUser?.email ||
@@ -264,14 +267,16 @@ export default function UsersTable({ users: u }: UsersTableProps) {
selectedUser?.username ||
""
}
title={t('userRemoveOrg')}
title={t("userRemoveOrg")}
/>
<UsersDataTable
columns={columns}
data={users}
inviteUser={() => {
router.push(`/${org?.org.orgId}/settings/access/users/create`);
router.push(
`/${org?.org.orgId}/settings/access/users/create`
);
}}
/>
</>

View File

@@ -5,17 +5,9 @@ import { authCookieHeader } from "@app/lib/api/cookies";
import { GetOrgUserResponse } from "@server/routers/user";
import OrgUserProvider from "@app/providers/OrgUserProvider";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import Link from "next/link";
import { cache } from "react";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { getTranslations } from 'next-intl/server';
import { getTranslations } from "next-intl/server";
interface UserLayoutProps {
children: React.ReactNode;
@@ -45,7 +37,7 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
const navItems = [
{
title: t('accessControls'),
title: t("accessControls"),
href: "/{orgId}/settings/access/users/{userId}/access-controls"
}
];
@@ -54,12 +46,10 @@ export default async function UserLayoutProps(props: UserLayoutProps) {
<>
<SettingsSectionTitle
title={`${user?.email}`}
description={t('userDescription2')}
description={t("userDescription2")}
/>
<OrgUserProvider orgUser={user}>
<HorizontalTabs items={navItems}>
{children}
</HorizontalTabs>
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
</OrgUserProvider>
</>
);

View File

@@ -78,40 +78,42 @@ export default function Page() {
const [dataLoaded, setDataLoaded] = useState(false);
const internalFormSchema = z.object({
email: z.string().email({ message: t('emailInvalid') }),
validForHours: z.string().min(1, { message: t('inviteValidityDuration') }),
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') })
email: z.string().email({ message: t("emailInvalid") }),
validForHours: z
.string()
.min(1, { message: t("inviteValidityDuration") }),
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") })
});
const externalFormSchema = z.object({
username: z.string().min(1, { message: t('usernameRequired') }),
username: z.string().min(1, { message: t("usernameRequired") }),
email: z
.string()
.email({ message: t('emailInvalid') })
.email({ message: t("emailInvalid") })
.optional()
.or(z.literal("")),
name: z.string().optional(),
roleId: z.string().min(1, { message: t('accessRoleSelectPlease') }),
idpId: z.string().min(1, { message: t('idpSelectPlease') })
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") }),
idpId: z.string().min(1, { message: t("idpSelectPlease") })
});
const formatIdpType = (type: string) => {
switch (type.toLowerCase()) {
case "oidc":
return t('idpGenericOidc');
return t("idpGenericOidc");
default:
return type;
}
};
const validFor = [
{ hours: 24, name: t('day', {count: 1}) },
{ hours: 48, name: t('day', {count: 2}) },
{ hours: 72, name: t('day', {count: 3}) },
{ hours: 96, name: t('day', {count: 4}) },
{ hours: 120, name: t('day', {count: 5}) },
{ hours: 144, name: t('day', {count: 6}) },
{ hours: 168, name: t('day', {count: 7}) }
{ hours: 24, name: t("day", { count: 1 }) },
{ hours: 48, name: t("day", { count: 2 }) },
{ hours: 72, name: t("day", { count: 3 }) },
{ hours: 96, name: t("day", { count: 4 }) },
{ hours: 120, name: t("day", { count: 5 }) },
{ hours: 144, name: t("day", { count: 6 }) },
{ hours: 168, name: t("day", { count: 7 }) }
];
const internalForm = useForm<z.infer<typeof internalFormSchema>>({
@@ -157,10 +159,10 @@ export default function Page() {
console.error(e);
toast({
variant: "destructive",
title: t('accessRoleErrorFetch'),
title: t("accessRoleErrorFetch"),
description: formatAxiosError(
e,
t('accessRoleErrorFetchDescription')
t("accessRoleErrorFetchDescription")
)
});
});
@@ -180,10 +182,10 @@ export default function Page() {
console.error(e);
toast({
variant: "destructive",
title: t('idpErrorFetch'),
title: t("idpErrorFetch"),
description: formatAxiosError(
e,
t('idpErrorFetchDescription')
t("idpErrorFetchDescription")
)
});
});
@@ -220,16 +222,16 @@ export default function Page() {
if (e.response?.status === 409) {
toast({
variant: "destructive",
title: t('userErrorExists'),
description: t('userErrorExistsDescription')
title: t("userErrorExists"),
description: t("userErrorExistsDescription")
});
} else {
toast({
variant: "destructive",
title: t('inviteError'),
title: t("inviteError"),
description: formatAxiosError(
e,
t('inviteErrorDescription')
t("inviteErrorDescription")
)
});
}
@@ -239,8 +241,8 @@ export default function Page() {
setInviteLink(res.data.data.inviteLink);
toast({
variant: "default",
title: t('userInvited'),
description: t('userInvitedDescription')
title: t("userInvited"),
description: t("userInvitedDescription")
});
setExpiresInDays(parseInt(values.validForHours) / 24);
@@ -266,10 +268,10 @@ export default function Page() {
.catch((e) => {
toast({
variant: "destructive",
title: t('userErrorCreate'),
title: t("userErrorCreate"),
description: formatAxiosError(
e,
t('userErrorCreateDescription')
t("userErrorCreateDescription")
)
});
});
@@ -277,8 +279,8 @@ export default function Page() {
if (res && res.status === 201) {
toast({
variant: "default",
title: t('userCreated'),
description: t('userCreatedDescription')
title: t("userCreated"),
description: t("userCreatedDescription")
});
router.push(`/${orgId}/settings/access/users`);
}
@@ -289,13 +291,13 @@ export default function Page() {
const userTypes: ReadonlyArray<UserTypeOption> = [
{
id: "internal",
title: t('userTypeInternal'),
description: t('userTypeInternalDescription')
title: t("userTypeInternal"),
description: t("userTypeInternalDescription")
},
{
id: "oidc",
title: t('userTypeExternal'),
description: t('userTypeExternalDescription')
title: t("userTypeExternal"),
description: t("userTypeExternalDescription")
}
];
@@ -303,8 +305,8 @@ export default function Page() {
<>
<div className="flex justify-between">
<HeaderTitle
title={t('accessUserCreate')}
description={t('accessUserCreateDescription')}
title={t("accessUserCreate")}
description={t("accessUserCreateDescription")}
/>
<Button
variant="outline"
@@ -312,219 +314,239 @@ export default function Page() {
router.push(`/${orgId}/settings/access/users`);
}}
>
{t('userSeeAll')}
{t("userSeeAll")}
</Button>
</div>
<div>
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('userTypeTitle')}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('userTypeDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<StrategySelect
options={userTypes}
defaultValue={userType || undefined}
onChange={(value) => {
setUserType(value as UserType);
if (value === "internal") {
internalForm.reset();
} else if (value === "oidc") {
externalForm.reset();
setSelectedIdp(null);
}
}}
cols={2}
/>
</SettingsSectionBody>
</SettingsSection>
{!inviteLink && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("userTypeTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("userTypeDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<StrategySelect
options={userTypes}
defaultValue={userType || undefined}
onChange={(value) => {
setUserType(value as UserType);
if (value === "internal") {
internalForm.reset();
} else if (value === "oidc") {
externalForm.reset();
setSelectedIdp(null);
}
}}
cols={2}
/>
</SettingsSectionBody>
</SettingsSection>
)}
{userType === "internal" && dataLoaded && (
<>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('userSettings')}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('userSettingsDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...internalForm}>
<form
onSubmit={internalForm.handleSubmit(
onSubmitInternal
)}
className="space-y-4"
id="create-user-form"
>
<FormField
control={
internalForm.control
}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('email')}
</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
{!inviteLink ? (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("userSettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("userSettingsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...internalForm}>
<form
onSubmit={internalForm.handleSubmit(
onSubmitInternal
)}
/>
{env.email.emailEnabled && (
<div className="flex items-center space-x-2">
<Checkbox
id="send-email"
checked={sendEmail}
onCheckedChange={(
e
) =>
setSendEmail(
e as boolean
)
}
/>
<label
htmlFor="send-email"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t('inviteEmailSent')}
</label>
</div>
)}
<FormField
control={
internalForm.control
}
name="validForHours"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('inviteValid')}
</FormLabel>
<Select
onValueChange={
field.onChange
}
defaultValue={
field.value
}
>
className="space-y-4"
id="create-user-form"
>
<FormField
control={
internalForm.control
}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("email")}
</FormLabel>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('selectDuration')} />
</SelectTrigger>
<Input
{...field}
/>
</FormControl>
<SelectContent>
{validFor.map(
(
option
) => (
<SelectItem
key={
option.hours
}
value={option.hours.toString()}
>
{
option.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={
internalForm.control
}
name="roleId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('role')}
</FormLabel>
<Select
onValueChange={
field.onChange
<FormField
control={
internalForm.control
}
name="validForHours"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"inviteValid"
)}
</FormLabel>
<Select
onValueChange={
field.onChange
}
defaultValue={
field.value
}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
"selectDuration"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{validFor.map(
(
option
) => (
<SelectItem
key={
option.hours
}
value={option.hours.toString()}
>
{
option.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={
internalForm.control
}
name="roleId"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("role")}
</FormLabel>
<Select
onValueChange={
field.onChange
}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
"accessRoleSelect"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{roles.map(
(
role
) => (
<SelectItem
key={
role.roleId
}
value={role.roleId.toString()}
>
{
role.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{env.email.emailEnabled && (
<div className="flex items-center space-x-2">
<Checkbox
id="send-email"
checked={sendEmail}
onCheckedChange={(
e
) =>
setSendEmail(
e as boolean
)
}
/>
<label
htmlFor="send-email"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('accessRoleSelect')} />
</SelectTrigger>
</FormControl>
<SelectContent>
{roles.map(
(
role
) => (
<SelectItem
key={
role.roleId
}
value={role.roleId.toString()}
>
{
role.name
}
</SelectItem>
)
)}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
{t(
"inviteEmailSent"
)}
</label>
</div>
)}
/>
{inviteLink && (
<div className="max-w-md space-y-4">
{sendEmail && (
<p>
{t('inviteEmailSentDescription')}
</p>
)}
{!sendEmail && (
<p>
{t('inviteSentDescription')}
</p>
)}
<p>
{t('inviteExpiresIn', {days: expiresInDays})}
</p>
<CopyTextBox
text={inviteLink}
wrapText={false}
/>
</div>
)}
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
) : (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("userInvited")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{sendEmail
? t("inviteEmailSentDescription")
: t("inviteSentDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<div className="space-y-4">
<p>
{t("inviteExpiresIn", {
days: expiresInDays
})}
</p>
<CopyTextBox
text={inviteLink}
wrapText={false}
/>
</div>
</SettingsSectionBody>
</SettingsSection>
)}
</>
)}
@@ -533,16 +555,16 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('idpTitle')}
{t("idpTitle")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('idpSelect')}
{t("idpSelect")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{idps.length === 0 ? (
<p className="text-muted-foreground">
{t('idpNotConfigured')}
{t("idpNotConfigured")}
</p>
) : (
<Form {...externalForm}>
@@ -596,10 +618,10 @@ export default function Page() {
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('userSettings')}
{t("userSettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('userSettingsDescription')}
{t("userSettingsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -620,7 +642,9 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('username')}
{t(
"username"
)}
</FormLabel>
<FormControl>
<Input
@@ -628,7 +652,9 @@ export default function Page() {
/>
</FormControl>
<p className="text-sm text-muted-foreground mt-1">
{t('usernameUniq')}
{t(
"usernameUniq"
)}
</p>
<FormMessage />
</FormItem>
@@ -643,7 +669,9 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('emailOptional')}
{t(
"emailOptional"
)}
</FormLabel>
<FormControl>
<Input
@@ -663,7 +691,9 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('nameOptional')}
{t(
"nameOptional"
)}
</FormLabel>
<FormControl>
<Input
@@ -683,7 +713,7 @@ export default function Page() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t('role')}
{t("role")}
</FormLabel>
<Select
onValueChange={
@@ -691,8 +721,12 @@ export default function Page() {
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('accessRoleSelect')} />
<SelectTrigger className="w-full">
<SelectValue
placeholder={t(
"accessRoleSelect"
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
@@ -736,19 +770,17 @@ export default function Page() {
router.push(`/${orgId}/settings/access/users`);
}}
>
{t('cancel')}
{t("cancel")}
</Button>
{userType && dataLoaded && (
<Button
type="submit"
form="create-user-form"
type={inviteLink ? "button" : "submit"}
form={inviteLink ? undefined : "create-user-form"}
loading={loading}
disabled={
loading ||
(userType === "internal" && inviteLink !== null)
}
disabled={loading}
onClick={inviteLink ? () => router.push(`/${orgId}/settings/access/users`) : undefined}
>
{t('accessUserCreate')}
{inviteLink ? t("done") : t("accessUserCreate")}
</Button>
)}
</div>

View File

@@ -50,11 +50,14 @@ export default function OrgApiKeysTable({
const deleteSite = (apiKeyId: string) => {
api.delete(`/org/${orgId}/api-key/${apiKeyId}`)
.catch((e) => {
console.error(t('apiKeysErrorDelete'), e);
console.error(t("apiKeysErrorDelete"), e);
toast({
variant: "destructive",
title: t('apiKeysErrorDelete'),
description: formatAxiosError(e, t('apiKeysErrorDeleteMessage'))
title: t("apiKeysErrorDelete"),
description: formatAxiosError(
e,
t("apiKeysErrorDeleteMessage")
)
});
})
.then(() => {
@@ -68,41 +71,6 @@ export default function OrgApiKeysTable({
};
const columns: ColumnDef<OrgApiKeyRow>[] = [
{
id: "dots",
cell: ({ row }) => {
const apiKeyROw = row.original;
const router = useRouter();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setSelected(apiKeyROw);
}}
>
<span>{t('viewSettings')}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelected(apiKeyROw);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
},
{
accessorKey: "name",
header: ({ column }) => {
@@ -113,7 +81,7 @@ export default function OrgApiKeysTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -121,7 +89,7 @@ export default function OrgApiKeysTable({
},
{
accessorKey: "key",
header: t('key'),
header: t("key"),
cell: ({ row }) => {
const r = row.original;
return <span className="font-mono">{r.key}</span>;
@@ -129,10 +97,10 @@ export default function OrgApiKeysTable({
},
{
accessorKey: "createdAt",
header: t('createdAt'),
header: t("createdAt"),
cell: ({ row }) => {
const r = row.original;
return <span>{moment(r.createdAt).format("lll")} </span>;
return <span>{moment(r.createdAt).format("lll")}</span>;
}
},
{
@@ -141,9 +109,43 @@ export default function OrgApiKeysTable({
const r = row.original;
return (
<div className="flex items-center justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setSelected(r);
}}
>
<span>{t("viewSettings")}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelected(r);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Link href={`/${orgId}/settings/api-keys/${r.id}`}>
<Button variant={"outlinePrimary"} className="ml-2">
{t('edit')}
<Button
variant={"secondary"}
className="ml-2"
size="sm"
>
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@@ -165,24 +167,23 @@ export default function OrgApiKeysTable({
dialog={
<div className="space-y-4">
<p>
{t('apiKeysQuestionRemove', {selectedApiKey: selected?.name || selected?.id})}
{t("apiKeysQuestionRemove", {
selectedApiKey:
selected?.name || selected?.id
})}
</p>
<p>
<b>
{t('apiKeysMessageRemove')}
</b>
<b>{t("apiKeysMessageRemove")}</b>
</p>
<p>
{t('apiKeysMessageConfirm')}
</p>
<p>{t("apiKeysMessageConfirm")}</p>
</div>
}
buttonText={t('apiKeysDeleteConfirm')}
buttonText={t("apiKeysDeleteConfirm")}
onConfirm={async () => deleteSite(selected!.id)}
string={selected.name}
title={t('apiKeysDelete')}
title={t("apiKeysDelete")}
/>
)}

View File

@@ -2,16 +2,7 @@ import { internal } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/lib/api/cookies";
import { SidebarSettings } from "@app/components/SidebarSettings";
import Link from "next/link";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import { GetApiKeyResponse } from "@server/routers/apiKeys";
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
import { HorizontalTabs } from "@app/components/HorizontalTabs";

View File

@@ -25,21 +25,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@app/components/ui/input";
import { InfoIcon } from "lucide-react";
import { Button } from "@app/components/ui/button";
import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios";
import { useParams, useRouter } from "next/navigation";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import Link from "next/link";
import {
CreateOrgApiKeyBody,
CreateOrgApiKeyResponse
@@ -110,7 +101,7 @@ export default function Page() {
const copiedForm = useForm<CopiedFormValues>({
resolver: zodResolver(copiedFormSchema),
defaultValues: {
copied: false
copied: true
}
});
@@ -308,54 +299,54 @@ export default function Page() {
</AlertDescription>
</Alert>
<h4 className="font-semibold">
{t('apiKeysInfo')}
</h4>
{/* <h4 className="font-semibold"> */}
{/* {t('apiKeysInfo')} */}
{/* </h4> */}
<CopyTextBox
text={`${apiKey.apiKeyId}.${apiKey.apiKey}`}
/>
<Form {...copiedForm}>
<form
className="space-y-4"
id="copied-form"
>
<FormField
control={copiedForm.control}
name="copied"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
defaultChecked={
copiedForm.getValues(
"copied"
) as boolean
}
onCheckedChange={(
e
) => {
copiedForm.setValue(
"copied",
e as boolean
);
}}
/>
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t('apiKeysConfirmCopy')}
</label>
</div>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
{/* <Form {...copiedForm}> */}
{/* <form */}
{/* className="space-y-4" */}
{/* id="copied-form" */}
{/* > */}
{/* <FormField */}
{/* control={copiedForm.control} */}
{/* name="copied" */}
{/* render={({ field }) => ( */}
{/* <FormItem> */}
{/* <div className="flex items-center space-x-2"> */}
{/* <Checkbox */}
{/* id="terms" */}
{/* defaultChecked={ */}
{/* copiedForm.getValues( */}
{/* "copied" */}
{/* ) as boolean */}
{/* } */}
{/* onCheckedChange={( */}
{/* e */}
{/* ) => { */}
{/* copiedForm.setValue( */}
{/* "copied", */}
{/* e as boolean */}
{/* ); */}
{/* }} */}
{/* /> */}
{/* <label */}
{/* htmlFor="terms" */}
{/* className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" */}
{/* > */}
{/* {t('apiKeysConfirmCopy')} */}
{/* </label> */}
{/* </div> */}
{/* <FormMessage /> */}
{/* </FormItem> */}
{/* )} */}
{/* /> */}
{/* </form> */}
{/* </Form> */}
</SettingsSectionBody>
</SettingsSection>
)}

View File

@@ -246,7 +246,7 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
<Link
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
>
<Button variant={"outlinePrimary"} className="ml-2">
<Button variant={"secondary"} className="ml-2">
Edit
<ArrowRight className="ml-2 w-4 h-4" />
</Button>

View File

@@ -1,7 +1,6 @@
import { internal } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { authCookieHeader } from "@app/lib/api/cookies";
import { SidebarSettings } from "@app/components/SidebarSettings";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { GetClientResponse } from "@server/routers/client";
import ClientInfoCard from "./ClientInfoCard";

View File

@@ -18,9 +18,9 @@ import { GetOrgUserResponse } from "@server/routers/user";
import UserProvider from "@app/providers/UserProvider";
import { Layout } from "@app/components/Layout";
import { SidebarNavItem, SidebarNavProps } from "@app/components/SidebarNav";
import { orgNavItems } from "@app/app/navigation";
import { getTranslations } from "next-intl/server";
import { pullEnv } from "@app/lib/pullEnv";
import { orgNavSections } from "@app/app/navigation";
export const dynamic = "force-dynamic";
@@ -82,24 +82,9 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
}
} catch (e) {}
if (env.flags.enableClients) {
const existing = orgNavItems.find(
(item) => item.title === "sidebarClients"
);
if (!existing) {
const clientsNavItem = {
title: "sidebarClients",
href: "/{orgId}/settings/clients",
icon: <Workflow className="h-4 w-4" />
};
orgNavItems.splice(1, 0, clientsNavItem);
}
}
return (
<UserProvider user={user}>
<Layout orgId={params.orgId} orgs={orgs} navItems={orgNavItems}>
<Layout orgId={params.orgId} orgs={orgs} navItems={orgNavSections}>
{children}
</Layout>
</UserProvider>

View File

@@ -31,7 +31,7 @@ import CopyToClipboard from "@app/components/CopyToClipboard";
import { Switch } from "@app/components/ui/switch";
import { AxiosResponse } from "axios";
import { UpdateResourceResponse } from "@server/routers/resource";
import { useTranslations } from 'next-intl';
import { useTranslations } from "next-intl";
export type ResourceRow = {
id: number;
@@ -65,11 +65,11 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
const deleteResource = (resourceId: number) => {
api.delete(`/resource/${resourceId}`)
.catch((e) => {
console.error(t('resourceErrorDelte'), e);
console.error(t("resourceErrorDelte"), e);
toast({
variant: "destructive",
title: t('resourceErrorDelte'),
description: formatAxiosError(e, t('resourceErrorDelte'))
title: t("resourceErrorDelte"),
description: formatAxiosError(e, t("resourceErrorDelte"))
});
})
.then(() => {
@@ -89,50 +89,16 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
.catch((e) => {
toast({
variant: "destructive",
title: t('resourcesErrorUpdate'),
description: formatAxiosError(e, t('resourcesErrorUpdateDescription'))
title: t("resourcesErrorUpdate"),
description: formatAxiosError(
e,
t("resourcesErrorUpdateDescription")
)
});
});
}
const columns: ColumnDef<ResourceRow>[] = [
{
accessorKey: "dots",
header: "",
cell: ({ row }) => {
const resourceRow = row.original;
const router = useRouter();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
>
<DropdownMenuItem>
{t('viewSettings')}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedResource(resourceRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
},
{
accessorKey: "name",
header: ({ column }) => {
@@ -143,7 +109,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -159,7 +125,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('site')}
{t("site")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -170,7 +136,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
<Link
href={`/${resourceRow.orgId}/settings/sites/${resourceRow.siteId}`}
>
<Button variant="outline">
<Button variant="outline" size="sm">
{resourceRow.site}
<ArrowUpRight className="ml-2 h-4 w-4" />
</Button>
@@ -180,7 +146,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
},
{
accessorKey: "protocol",
header: t('protocol'),
header: t("protocol"),
cell: ({ row }) => {
const resourceRow = row.original;
return <span>{resourceRow.protocol.toUpperCase()}</span>;
@@ -188,7 +154,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
},
{
accessorKey: "domain",
header: t('access'),
header: t("access"),
cell: ({ row }) => {
const resourceRow = row.original;
return (
@@ -218,7 +184,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('authentication')}
{t("authentication")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -230,12 +196,12 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
{resourceRow.authState === "protected" ? (
<span className="text-green-500 flex items-center space-x-2">
<ShieldCheck className="w-4 h-4" />
<span>{t('protected')}</span>
<span>{t("protected")}</span>
</span>
) : resourceRow.authState === "not_protected" ? (
<span className="text-yellow-500 flex items-center space-x-2">
<ShieldOff className="w-4 h-4" />
<span>{t('notProtected')}</span>
<span>{t("notProtected")}</span>
</span>
) : (
<span>-</span>
@@ -246,7 +212,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
},
{
accessorKey: "enabled",
header: t('enabled'),
header: t("enabled"),
cell: ({ row }) => (
<Switch
defaultChecked={row.original.enabled}
@@ -262,11 +228,41 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
const resourceRow = row.original;
return (
<div className="flex items-center justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
>
<DropdownMenuItem>
{t("viewSettings")}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedResource(resourceRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Link
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
>
<Button variant={"outlinePrimary"} className="ml-2">
{t('edit')}
<Button variant={"secondary"} className="ml-2" size="sm">
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@@ -288,22 +284,22 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
dialog={
<div>
<p className="mb-2">
{t('resourceQuestionRemove', {selectedResource: selectedResource?.name || selectedResource?.id})}
{t("resourceQuestionRemove", {
selectedResource:
selectedResource?.name ||
selectedResource?.id
})}
</p>
<p className="mb-2">
{t('resourceMessageRemove')}
</p>
<p className="mb-2">{t("resourceMessageRemove")}</p>
<p>
{t('resourceMessageConfirm')}
</p>
<p>{t("resourceMessageConfirm")}</p>
</div>
}
buttonText={t('resourceDeleteConfirm')}
buttonText={t("resourceDeleteConfirm")}
onConfirm={async () => deleteResource(selectedResource!.id)}
string={selectedResource.name}
title={t('resourceDelete')}
title={t("resourceDelete")}
/>
)}

View File

@@ -568,7 +568,7 @@ export default function ResourceAuthenticationPage() {
</span>
</div>
<Button
variant="outlinePrimary"
variant="secondary"
onClick={
authInfo.password
? removeResourcePassword
@@ -593,7 +593,7 @@ export default function ResourceAuthenticationPage() {
</span>
</div>
<Button
variant="outlinePrimary"
variant="secondary"
onClick={
authInfo.pincode
? removeResourcePincode

View File

@@ -128,7 +128,7 @@ export default function ReverseProxyTargets(props: {
return true;
},
{
message: t('proxyErrorInvalidHeader')
message: t("proxyErrorInvalidHeader")
}
)
});
@@ -146,7 +146,7 @@ export default function ReverseProxyTargets(props: {
return true;
},
{
message: t('proxyErrorTls')
message: t("proxyErrorTls")
}
)
});
@@ -203,10 +203,10 @@ export default function ReverseProxyTargets(props: {
console.error(err);
toast({
variant: "destructive",
title: t('targetErrorFetch'),
title: t("targetErrorFetch"),
description: formatAxiosError(
err,
t('targetErrorFetchDescription')
t("targetErrorFetchDescription")
)
});
} finally {
@@ -228,10 +228,10 @@ export default function ReverseProxyTargets(props: {
console.error(err);
toast({
variant: "destructive",
title: t('siteErrorFetch'),
title: t("siteErrorFetch"),
description: formatAxiosError(
err,
t('siteErrorFetchDescription')
t("siteErrorFetchDescription")
)
});
}
@@ -251,8 +251,8 @@ export default function ReverseProxyTargets(props: {
if (isDuplicate) {
toast({
variant: "destructive",
title: t('targetErrorDuplicate'),
description: t('targetErrorDuplicateDescription')
title: t("targetErrorDuplicate"),
description: t("targetErrorDuplicateDescription")
});
return;
}
@@ -264,8 +264,8 @@ export default function ReverseProxyTargets(props: {
if (!isIPInSubnet(targetIp, subnet)) {
toast({
variant: "destructive",
title: t('targetWireGuardErrorInvalidIp'),
description: t('targetWireGuardErrorInvalidIpDescription')
title: t("targetWireGuardErrorInvalidIp"),
description: t("targetWireGuardErrorInvalidIpDescription")
});
return;
}
@@ -307,10 +307,13 @@ export default function ReverseProxyTargets(props: {
);
}
async function saveTargets() {
async function saveAllSettings() {
try {
setTargetsLoading(true);
setHttpsTlsLoading(true);
setProxySettingsLoading(true);
// Save targets
for (let target of targets) {
const data = {
ip: target.ip,
@@ -342,9 +345,31 @@ export default function ReverseProxyTargets(props: {
});
updateResource({ stickySession: stickySessionData.stickySession });
// Save TLS settings
const tlsData = tlsSettingsForm.getValues();
await api.post(`/resource/${params.resourceId}`, {
ssl: tlsData.ssl,
tlsServerName: tlsData.tlsServerName || null
});
updateResource({
...resource,
ssl: tlsData.ssl,
tlsServerName: tlsData.tlsServerName || null
});
// Save proxy settings
const proxyData = proxySettingsForm.getValues();
await api.post(`/resource/${params.resourceId}`, {
setHostHeader: proxyData.setHostHeader || null
});
updateResource({
...resource,
setHostHeader: proxyData.setHostHeader || null
});
toast({
title: t('targetsUpdated'),
description: t('targetsUpdatedDescription')
title: t("settingsUpdated"),
description: t("settingsUpdatedDescription")
});
setTargetsToRemove([]);
@@ -353,73 +378,15 @@ export default function ReverseProxyTargets(props: {
console.error(err);
toast({
variant: "destructive",
title: t('targetsErrorUpdate'),
title: t("settingsErrorUpdate"),
description: formatAxiosError(
err,
t('targetsErrorUpdateDescription')
t("settingsErrorUpdateDescription")
)
});
} finally {
setTargetsLoading(false);
}
}
async function saveTlsSettings(data: TlsSettingsValues) {
try {
setHttpsTlsLoading(true);
await api.post(`/resource/${params.resourceId}`, {
ssl: data.ssl,
tlsServerName: data.tlsServerName || null
});
updateResource({
...resource,
ssl: data.ssl,
tlsServerName: data.tlsServerName || null
});
toast({
title: t('targetTlsUpdate'),
description: t('targetTlsUpdateDescription')
});
} catch (err) {
console.error(err);
toast({
variant: "destructive",
title: t('targetErrorTlsUpdate'),
description: formatAxiosError(
err,
t('targetErrorTlsUpdateDescription')
)
});
} finally {
setHttpsTlsLoading(false);
}
}
async function saveProxySettings(data: ProxySettingsValues) {
try {
setProxySettingsLoading(true);
await api.post(`/resource/${params.resourceId}`, {
setHostHeader: data.setHostHeader || null
});
updateResource({
...resource,
setHostHeader: data.setHostHeader || null
});
toast({
title: t('proxyUpdated'),
description: t('proxyUpdatedDescription')
});
} catch (err) {
console.error(err);
toast({
variant: "destructive",
title: t('proxyErrorUpdate'),
description: formatAxiosError(
err,
t('proxyErrorUpdateDescription')
)
});
} finally {
setProxySettingsLoading(false);
}
}
@@ -427,7 +394,7 @@ export default function ReverseProxyTargets(props: {
const columns: ColumnDef<LocalTarget>[] = [
{
accessorKey: "ip",
header: t('targetAddr'),
header: t("targetAddr"),
cell: ({ row }) => (
<Input
defaultValue={row.original.ip}
@@ -442,7 +409,7 @@ export default function ReverseProxyTargets(props: {
},
{
accessorKey: "port",
header: t('targetPort'),
header: t("targetPort"),
cell: ({ row }) => (
<Input
type="number"
@@ -476,7 +443,7 @@ export default function ReverseProxyTargets(props: {
// },
{
accessorKey: "enabled",
header: t('enabled'),
header: t("enabled"),
cell: ({ row }) => (
<Switch
defaultChecked={row.original.enabled}
@@ -503,7 +470,7 @@ export default function ReverseProxyTargets(props: {
variant="outline"
onClick={() => removeTarget(row.original.targetId)}
>
{t('delete')}
{t("delete")}
</Button>
</div>
</>
@@ -514,7 +481,7 @@ export default function ReverseProxyTargets(props: {
if (resource.http) {
const methodCol: ColumnDef<LocalTarget> = {
accessorKey: "method",
header: t('method'),
header: t("method"),
cell: ({ row }) => (
<Select
defaultValue={row.original.method ?? ""}
@@ -561,11 +528,9 @@ export default function ReverseProxyTargets(props: {
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('targets')}
</SettingsSectionTitle>
<SettingsSectionTitle>{t("targets")}</SettingsSectionTitle>
<SettingsSectionDescription>
{t('targetsDescription')}
{t("targetsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
@@ -573,7 +538,7 @@ export default function ReverseProxyTargets(props: {
<Form {...targetsSettingsForm}>
<form
onSubmit={targetsSettingsForm.handleSubmit(
saveTargets
saveAllSettings
)}
className="space-y-4"
id="targets-settings-form"
@@ -587,8 +552,12 @@ export default function ReverseProxyTargets(props: {
<FormControl>
<SwitchInput
id="sticky-toggle"
label={t('targetStickySessions')}
description={t('targetStickySessionsDescription')}
label={t(
"targetStickySessions"
)}
description={t(
"targetStickySessionsDescription"
)}
defaultChecked={
field.value
}
@@ -619,7 +588,9 @@ export default function ReverseProxyTargets(props: {
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>{t('method')}</FormLabel>
<FormLabel>
{t("method")}
</FormLabel>
<FormControl>
<Select
value={
@@ -635,8 +606,15 @@ export default function ReverseProxyTargets(props: {
);
}}
>
<SelectTrigger id="method">
<SelectValue placeholder={t('methodSelect')} />
<SelectTrigger
id="method"
className="w-full"
>
<SelectValue
placeholder={t(
"methodSelect"
)}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="http">
@@ -662,7 +640,9 @@ export default function ReverseProxyTargets(props: {
name="ip"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>{t('targetAddr')}</FormLabel>
<FormLabel>
{t("targetAddr")}
</FormLabel>
<FormControl>
<Input id="ip" {...field} />
</FormControl>
@@ -695,7 +675,9 @@ export default function ReverseProxyTargets(props: {
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>{t('targetPort')}</FormLabel>
<FormLabel>
{t("targetPort")}
</FormLabel>
<FormControl>
<Input
id="port"
@@ -710,11 +692,11 @@ export default function ReverseProxyTargets(props: {
/>
<Button
type="submit"
variant="outlinePrimary"
variant="secondary"
className="mt-6"
disabled={!(watchedIp && watchedPort)}
>
{t('targetSubmit')}
{t("targetSubmit")}
</Button>
</div>
</form>
@@ -758,189 +740,170 @@ export default function ReverseProxyTargets(props: {
colSpan={columns.length}
className="h-24 text-center"
>
{t('targetNoOne')}
{t("targetNoOne")}
</TableCell>
</TableRow>
)}
</TableBody>
<TableCaption>
{t('targetNoOneDescription')}
</TableCaption>
{/* <TableCaption> */}
{/* {t('targetNoOneDescription')} */}
{/* </TableCaption> */}
</Table>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
onClick={saveTargets}
loading={targetsLoading}
disabled={targetsLoading}
form="targets-settings-form"
>
{t('targetsSubmit')}
</Button>
</SettingsSectionFooter>
</SettingsSection>
{resource.http && (
<SettingsSectionGrid cols={2}>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('targetTlsSettings')}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('targetTlsSettingsDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...tlsSettingsForm}>
<form
onSubmit={tlsSettingsForm.handleSubmit(
saveTlsSettings
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("proxyAdditional")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("proxyAdditionalDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...tlsSettingsForm}>
<form
onSubmit={tlsSettingsForm.handleSubmit(
saveAllSettings
)}
className="space-y-4"
id="tls-settings-form"
>
<FormField
control={tlsSettingsForm.control}
name="ssl"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="ssl-toggle"
label={t(
"proxyEnableSSL"
)}
defaultChecked={
field.value
}
onCheckedChange={(
val
) => {
field.onChange(val);
}}
/>
</FormControl>
</FormItem>
)}
className="space-y-4"
id="tls-settings-form"
/>
<Collapsible
open={isAdvancedOpen}
onOpenChange={setIsAdvancedOpen}
className="space-y-2"
>
<FormField
control={tlsSettingsForm.control}
name="ssl"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="ssl-toggle"
label={t('proxyEnableSSL')}
defaultChecked={
field.value
}
onCheckedChange={(
val
) => {
field.onChange(
val
);
}}
/>
</FormControl>
</FormItem>
)}
/>
<Collapsible
open={isAdvancedOpen}
onOpenChange={setIsAdvancedOpen}
className="space-y-2"
>
<div className="flex items-center justify-between space-x-4">
<CollapsibleTrigger asChild>
<Button
variant="text"
size="sm"
className="p-0 flex items-center justify-start gap-2 w-full"
>
<p className="text-sm text-muted-foreground">
{t('targetTlsSettingsAdvanced')}
</p>
<div>
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">
Toggle
</span>
</div>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="space-y-2">
<FormField
control={
tlsSettingsForm.control
}
name="tlsServerName"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('targetTlsSni')}
</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormDescription>
{t('targetTlsSniDescription')}
</FormDescription>
<FormMessage />
</FormItem>
<div className="flex items-center justify-between space-x-4">
<CollapsibleTrigger asChild>
<Button
variant="text"
size="sm"
className="p-0 flex items-center justify-start gap-2 w-full"
>
<p className="text-sm text-muted-foreground">
{t(
"targetTlsSettingsAdvanced"
)}
</p>
<div>
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">
Toggle
</span>
</div>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="space-y-2">
<FormField
control={
tlsSettingsForm.control
}
name="tlsServerName"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("targetTlsSni")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"targetTlsSniDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CollapsibleContent>
</Collapsible>
</form>
</Form>
</SettingsSectionForm>
<SettingsSectionForm>
<Form {...proxySettingsForm}>
<form
onSubmit={proxySettingsForm.handleSubmit(
saveAllSettings
)}
className="space-y-4"
id="proxy-settings-form"
>
<FormField
control={proxySettingsForm.control}
name="setHostHeader"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("proxyCustomHeader")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t(
"proxyCustomHeaderDescription"
)}
/>
</CollapsibleContent>
</Collapsible>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={httpsTlsLoading}
form="tls-settings-form"
>
{t('targetTlsSubmit')}
</Button>
</SettingsSectionFooter>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('proxyAdditional')}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('proxyAdditionalDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...proxySettingsForm}>
<form
onSubmit={proxySettingsForm.handleSubmit(
saveProxySettings
</FormDescription>
<FormMessage />
</FormItem>
)}
className="space-y-4"
id="proxy-settings-form"
>
<FormField
control={proxySettingsForm.control}
name="setHostHeader"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('proxyCustomHeader')}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
{t('proxyCustomHeaderDescription')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={proxySettingsLoading}
form="proxy-settings-form"
>
{t('targetTlsSubmit')}
</Button>
</SettingsSectionFooter>
</SettingsSection>
</SettingsSectionGrid>
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
)}
<div className="flex justify-end mt-6">
<Button
onClick={saveAllSettings}
loading={
targetsLoading ||
httpsTlsLoading ||
proxySettingsLoading
}
disabled={
targetsLoading ||
httpsTlsLoading ||
proxySettingsLoading
}
>
{t("saveAllSettings")}
</Button>
</div>
</SettingsContainer>
);
}
@@ -953,7 +916,7 @@ function isIPInSubnet(subnet: string, ip: string): boolean {
const t = useTranslations();
if (mask < 0 || mask > 32) {
throw new Error(t('subnetMaskErrorInvalid'));
throw new Error(t("subnetMaskErrorInvalid"));
}
// Convert IP addresses to binary numbers
@@ -973,14 +936,14 @@ function ipToNumber(ip: string): number {
const t = useTranslations();
if (parts.length !== 4) {
throw new Error(t('ipAddressErrorInvalidFormat'));
throw new Error(t("ipAddressErrorInvalidFormat"));
}
// Convert IP octets to 32-bit number
return parts.reduce((num, octet) => {
const oct = parseInt(octet);
if (isNaN(oct) || oct < 0 || oct > 255) {
throw new Error(t('ipAddressErrorInvalidOctet'));
throw new Error(t("ipAddressErrorInvalidOctet"));
}
return (num << 8) + oct;
}, 0);

View File

@@ -36,7 +36,6 @@ import {
TableBody,
TableCaption,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow
@@ -543,48 +542,48 @@ export default function ResourceRules(props: {
return (
<SettingsContainer>
<Alert className="hidden md:block">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">{t('rulesAbout')}</AlertTitle>
<AlertDescription className="mt-4">
<div className="space-y-1 mb-4">
<p>
{t('rulesAboutDescription')}
</p>
</div>
<InfoSections cols={2}>
<InfoSection>
<InfoSectionTitle>{t('rulesActions')}</InfoSectionTitle>
<ul className="text-sm text-muted-foreground space-y-1">
<li className="flex items-center gap-2">
<Check className="text-green-500 w-4 h-4" />
{t('rulesActionAlwaysAllow')}
</li>
<li className="flex items-center gap-2">
<X className="text-red-500 w-4 h-4" />
{t('rulesActionAlwaysDeny')}
</li>
</ul>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t('rulesMatchCriteria')}
</InfoSectionTitle>
<ul className="text-sm text-muted-foreground space-y-1">
<li className="flex items-center gap-2">
{t('rulesMatchCriteriaIpAddress')}
</li>
<li className="flex items-center gap-2">
{t('rulesMatchCriteriaIpAddressRange')}
</li>
<li className="flex items-center gap-2">
{t('rulesMatchCriteriaUrl')}
</li>
</ul>
</InfoSection>
</InfoSections>
</AlertDescription>
</Alert>
{/* <Alert className="hidden md:block"> */}
{/* <InfoIcon className="h-4 w-4" /> */}
{/* <AlertTitle className="font-semibold">{t('rulesAbout')}</AlertTitle> */}
{/* <AlertDescription className="mt-4"> */}
{/* <div className="space-y-1 mb-4"> */}
{/* <p> */}
{/* {t('rulesAboutDescription')} */}
{/* </p> */}
{/* </div> */}
{/* <InfoSections cols={2}> */}
{/* <InfoSection> */}
{/* <InfoSectionTitle>{t('rulesActions')}</InfoSectionTitle> */}
{/* <ul className="text-sm text-muted-foreground space-y-1"> */}
{/* <li className="flex items-center gap-2"> */}
{/* <Check className="text-green-500 w-4 h-4" /> */}
{/* {t('rulesActionAlwaysAllow')} */}
{/* </li> */}
{/* <li className="flex items-center gap-2"> */}
{/* <X className="text-red-500 w-4 h-4" /> */}
{/* {t('rulesActionAlwaysDeny')} */}
{/* </li> */}
{/* </ul> */}
{/* </InfoSection> */}
{/* <InfoSection> */}
{/* <InfoSectionTitle> */}
{/* {t('rulesMatchCriteria')} */}
{/* </InfoSectionTitle> */}
{/* <ul className="text-sm text-muted-foreground space-y-1"> */}
{/* <li className="flex items-center gap-2"> */}
{/* {t('rulesMatchCriteriaIpAddress')} */}
{/* </li> */}
{/* <li className="flex items-center gap-2"> */}
{/* {t('rulesMatchCriteriaIpAddressRange')} */}
{/* </li> */}
{/* <li className="flex items-center gap-2"> */}
{/* {t('rulesMatchCriteriaUrl')} */}
{/* </li> */}
{/* </ul> */}
{/* </InfoSection> */}
{/* </InfoSections> */}
{/* </AlertDescription> */}
{/* </Alert> */}
<SettingsSection>
<SettingsSectionHeader>
@@ -634,7 +633,7 @@ export default function ResourceRules(props: {
field.onChange
}
>
<SelectTrigger>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -664,7 +663,7 @@ export default function ResourceRules(props: {
field.onChange
}
>
<SelectTrigger>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -690,7 +689,7 @@ export default function ResourceRules(props: {
control={addRuleForm.control}
name="value"
render={({ field }) => (
<FormItem className="space-y-0 mb-2">
<FormItem className="gap-1">
<InfoPopup
text={t('value')}
info={
@@ -702,7 +701,7 @@ export default function ResourceRules(props: {
}
/>
<FormControl>
<Input {...field} />
<Input {...field}/>
</FormControl>
<FormMessage />
</FormItem>
@@ -710,8 +709,7 @@ export default function ResourceRules(props: {
/>
<Button
type="submit"
variant="outlinePrimary"
className="mb-2"
variant="secondary"
disabled={!rulesEnabled}
>
{t('ruleSubmit')}
@@ -762,9 +760,9 @@ export default function ResourceRules(props: {
</TableRow>
)}
</TableBody>
<TableCaption>
{t('rulesOrder')}
</TableCaption>
{/* <TableCaption> */}
{/* {t('rulesOrder')} */}
{/* </TableCaption> */}
</Table>
</SettingsSectionBody>
<SettingsSectionFooter>

View File

@@ -459,29 +459,31 @@ export default function Page() {
</SettingsSectionBody>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("resourceType")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("resourceTypeDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<StrategySelect
options={resourceTypes}
defaultValue="http"
onChange={(value) => {
baseForm.setValue(
"http",
value === "http"
);
}}
cols={2}
/>
</SettingsSectionBody>
</SettingsSection>
{resourceTypes.length > 1 && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("resourceType")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("resourceTypeDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<StrategySelect
options={resourceTypes}
defaultValue="http"
onChange={(value) => {
baseForm.setValue(
"http",
value === "http"
);
}}
cols={2}
/>
</SettingsSectionBody>
</SettingsSection>
)}
{baseForm.watch("http") ? (
<SettingsSection>

View File

@@ -392,7 +392,7 @@ export default function CreateShareLinkForm({
defaultValue={field.value.toString()}
>
<FormControl>
<SelectTrigger>
<SelectTrigger className="w-full">
<SelectValue placeholder={t('selectDuration')} />
</SelectTrigger>
</FormControl>

View File

@@ -69,11 +69,8 @@ export default function ShareLinksTable({
async function deleteSharelink(id: string) {
await api.delete(`/access-token/${id}`).catch((e) => {
toast({
title: t('shareErrorDelete'),
description: formatAxiosError(
e,
t('shareErrorDeleteMessage')
)
title: t("shareErrorDelete"),
description: formatAxiosError(e, t("shareErrorDeleteMessage"))
});
});
@@ -81,53 +78,12 @@ export default function ShareLinksTable({
setRows(newRows);
toast({
title: t('shareDeleted'),
description: t('shareDeletedDescription')
title: t("shareDeleted"),
description: t("shareDeletedDescription")
});
}
const columns: ColumnDef<ShareLinkRow>[] = [
{
id: "actions",
cell: ({ row }) => {
const router = useRouter();
const resourceRow = row.original;
return (
<>
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
{t('openMenu')}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
deleteSharelink(
resourceRow.accessTokenId
);
}}
>
<button className="text-red-500">
{t('delete')}
</button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
);
}
},
{
accessorKey: "resourceName",
header: ({ column }) => {
@@ -138,7 +94,7 @@ export default function ShareLinksTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('resource')}
{t("resource")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -147,7 +103,7 @@ export default function ShareLinksTable({
const r = row.original;
return (
<Link href={`/${orgId}/settings/resources/${r.resourceId}`}>
<Button variant="outline">
<Button variant="outline" size="sm">
{r.resourceName}{" "}
{r.siteName ? `(${r.siteName})` : ""}
<ArrowUpRight className="ml-2 h-4 w-4" />
@@ -166,7 +122,7 @@ export default function ShareLinksTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('title')}
{t("title")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -245,7 +201,7 @@ export default function ShareLinksTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('created')}
{t("created")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -265,7 +221,7 @@ export default function ShareLinksTable({
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('expires')}
{t("expires")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -275,23 +231,50 @@ export default function ShareLinksTable({
if (r.expiresAt) {
return moment(r.expiresAt).format("lll");
}
return t('never');
return t("never");
}
},
{
id: "delete",
cell: ({ row }) => (
<div className="flex items-center justify-end space-x-2">
<Button
variant="outlinePrimary"
onClick={() =>
deleteSharelink(row.original.accessTokenId)
}
>
{t('delete')}
</Button>
</div>
)
cell: ({ row }) => {
const resourceRow = row.original;
return (
<div className="flex items-center justify-end space-x-2">
{/* <DropdownMenu> */}
{/* <DropdownMenuTrigger asChild> */}
{/* <Button variant="ghost" className="h-8 w-8 p-0"> */}
{/* <span className="sr-only"> */}
{/* {t("openMenu")} */}
{/* </span> */}
{/* <MoreHorizontal className="h-4 w-4" /> */}
{/* </Button> */}
{/* </DropdownMenuTrigger> */}
{/* <DropdownMenuContent align="end"> */}
{/* <DropdownMenuItem */}
{/* onClick={() => { */}
{/* deleteSharelink( */}
{/* resourceRow.accessTokenId */}
{/* ); */}
{/* }} */}
{/* > */}
{/* <button className="text-red-500"> */}
{/* {t("delete")} */}
{/* </button> */}
{/* </DropdownMenuItem> */}
{/* </DropdownMenuContent> */}
{/* </DropdownMenu> */}
<Button
variant="secondary"
size="sm"
onClick={() =>
deleteSharelink(row.original.accessTokenId)
}
>
{t("delete")}
</Button>
</div>
);
}
}
];

View File

@@ -28,7 +28,7 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import CreateSiteFormModal from "./CreateSiteModal";
import { useTranslations } from "next-intl";
import { parseDataSize } from '@app/lib/dataSize';
import { parseDataSize } from "@app/lib/dataSize";
export type SiteRow = {
id: number;
@@ -81,11 +81,11 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
const deleteSite = (siteId: number) => {
api.delete(`/site/${siteId}`)
.catch((e) => {
console.error(t('siteErrorDelete'), e);
console.error(t("siteErrorDelete"), e);
toast({
variant: "destructive",
title: t('siteErrorDelete'),
description: formatAxiosError(e, t('siteErrorDelete'))
title: t("siteErrorDelete"),
description: formatAxiosError(e, t("siteErrorDelete"))
});
})
.then(() => {
@@ -99,42 +99,6 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
};
const columns: ColumnDef<SiteRow>[] = [
{
id: "dots",
cell: ({ row }) => {
const siteRow = row.original;
const router = useRouter();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
>
<DropdownMenuItem>
{t('viewSettings')}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedSite(siteRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
},
{
accessorKey: "name",
header: ({ column }) => {
@@ -145,7 +109,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -161,7 +125,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('online')}
{t("online")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -176,14 +140,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
return (
<span className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>{t('online')}</span>
<span>{t("online")}</span>
</span>
);
} else {
return (
<span className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>{t('offline')}</span>
<span>{t("offline")}</span>
</span>
);
}
@@ -203,7 +167,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
}
className="hidden md:flex whitespace-nowrap"
>
{t('site')}
{t("site")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -226,13 +190,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('dataIn')}
{t("dataIn")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
sortingFn: (rowA, rowB) =>
parseDataSize(rowA.original.mbIn) - parseDataSize(rowB.original.mbIn)
sortingFn: (rowA, rowB) =>
parseDataSize(rowA.original.mbIn) -
parseDataSize(rowB.original.mbIn)
},
{
accessorKey: "mbOut",
@@ -244,13 +209,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('dataOut')}
{t("dataOut")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
sortingFn: (rowA, rowB) =>
parseDataSize(rowA.original.mbOut) - parseDataSize(rowB.original.mbOut),
parseDataSize(rowA.original.mbOut) -
parseDataSize(rowB.original.mbOut)
},
{
accessorKey: "type",
@@ -262,7 +228,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('connectionType')}
{t("connectionType")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -289,7 +255,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
if (originalRow.type === "local") {
return (
<div className="flex items-center space-x-2">
<span>{t('local')}</span>
<span>{t("local")}</span>
</div>
);
}
@@ -316,12 +282,41 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
cell: ({ row }) => {
const siteRow = row.original;
return (
<div className="flex items-center justify-end">
<div className="flex items-center justify-end gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
>
<DropdownMenuItem>
{t("viewSettings")}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedSite(siteRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Link
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
>
<Button variant={"outlinePrimary"} className="ml-2">
{t('edit')}
<Button variant={"secondary"} size="sm">
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@@ -343,22 +338,21 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
dialog={
<div className="space-y-4">
<p>
{t('siteQuestionRemove', {selectedSite: selectedSite?.name || selectedSite?.id})}
{t("siteQuestionRemove", {
selectedSite:
selectedSite?.name || selectedSite?.id
})}
</p>
<p>
{t('siteMessageRemove')}
</p>
<p>{t("siteMessageRemove")}</p>
<p>
{t('siteMessageConfirm')}
</p>
<p>{t("siteMessageConfirm")}</p>
</div>
}
buttonText={t('siteConfirmDelete')}
buttonText={t("siteConfirmDelete")}
onConfirm={async () => deleteSite(selectedSite!.id)}
string={selectedSite.name}
title={t('siteDelete')}
title={t("siteDelete")}
/>
)}

View File

@@ -5,16 +5,7 @@ import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/lib/api/cookies";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import SiteInfoCard from "./SiteInfoCard";
import { getTranslations } from "next-intl/server";

View File

@@ -55,14 +55,6 @@ import {
import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios";
import { useParams, useRouter } from "next/navigation";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import Link from "next/link";
import { QRCodeCanvas } from "qrcode.react";
import { useTranslations } from "next-intl";
@@ -117,7 +109,8 @@ export default function Page() {
.refine(
(data) => {
if (data.method !== "local") {
return data.copied;
// return data.copied;
return true;
}
return true;
},
@@ -622,26 +615,28 @@ WantedBy=default.target`
</SettingsSectionBody>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('tunnelType')}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('siteTunnelDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<StrategySelect
options={tunnelTypes}
defaultValue={form.getValues("method")}
onChange={(value) => {
form.setValue("method", value);
}}
cols={3}
/>
</SettingsSectionBody>
</SettingsSection>
{tunnelTypes.length > 1 && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t('tunnelType')}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t('siteTunnelDescription')}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<StrategySelect
options={tunnelTypes}
defaultValue={form.getValues("method")}
onChange={(value) => {
form.setValue("method", value);
}}
cols={3}
/>
</SettingsSectionBody>
</SettingsSection>
)}
{form.watch("method") === "newt" && (
<>
@@ -700,46 +695,46 @@ WantedBy=default.target`
</AlertDescription>
</Alert>
<Form {...form}>
<form
className="space-y-4"
id="create-site-form"
>
<FormField
control={form.control}
name="copied"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
defaultChecked={
form.getValues(
"copied"
) as boolean
}
onCheckedChange={(
e
) => {
form.setValue(
"copied",
e as boolean
);
}}
/>
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t('siteConfirmCopy')}
</label>
</div>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
{/* <Form {...form}> */}
{/* <form */}
{/* className="space-y-4" */}
{/* id="create-site-form" */}
{/* > */}
{/* <FormField */}
{/* control={form.control} */}
{/* name="copied" */}
{/* render={({ field }) => ( */}
{/* <FormItem> */}
{/* <div className="flex items-center space-x-2"> */}
{/* <Checkbox */}
{/* id="terms" */}
{/* defaultChecked={ */}
{/* form.getValues( */}
{/* "copied" */}
{/* ) as boolean */}
{/* } */}
{/* onCheckedChange={( */}
{/* e */}
{/* ) => { */}
{/* form.setValue( */}
{/* "copied", */}
{/* e as boolean */}
{/* ); */}
{/* }} */}
{/* /> */}
{/* <label */}
{/* htmlFor="terms" */}
{/* className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" */}
{/* > */}
{/* {t('siteConfirmCopy')} */}
{/* </label> */}
{/* </div> */}
{/* <FormMessage /> */}
{/* </FormItem> */}
{/* )} */}
{/* /> */}
{/* </form> */}
{/* </Form> */}
</SettingsSectionBody>
</SettingsSection>
<SettingsSection>

View File

@@ -46,11 +46,14 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
const deleteSite = (apiKeyId: string) => {
api.delete(`/api-key/${apiKeyId}`)
.catch((e) => {
console.error(t('apiKeysErrorDelete'), e);
console.error(t("apiKeysErrorDelete"), e);
toast({
variant: "destructive",
title: t('apiKeysErrorDelete'),
description: formatAxiosError(e, t('apiKeysErrorDeleteMessage'))
title: t("apiKeysErrorDelete"),
description: formatAxiosError(
e,
t("apiKeysErrorDeleteMessage")
)
});
})
.then(() => {
@@ -64,41 +67,6 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
};
const columns: ColumnDef<ApiKeyRow>[] = [
{
id: "dots",
cell: ({ row }) => {
const apiKeyROw = row.original;
const router = useRouter();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setSelected(apiKeyROw);
}}
>
<span>{t('viewSettings')}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelected(apiKeyROw);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
},
{
accessorKey: "name",
header: ({ column }) => {
@@ -109,7 +77,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -117,7 +85,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
},
{
accessorKey: "key",
header: t('key'),
header: t("key"),
cell: ({ row }) => {
const r = row.original;
return <span className="font-mono">{r.key}</span>;
@@ -125,7 +93,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
},
{
accessorKey: "createdAt",
header: t('createdAt'),
header: t("createdAt"),
cell: ({ row }) => {
const r = row.original;
return <span>{moment(r.createdAt).format("lll")} </span>;
@@ -136,13 +104,44 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
cell: ({ row }) => {
const r = row.original;
return (
<div className="flex items-center justify-end">
<Link href={`/admin/api-keys/${r.id}`}>
<Button variant={"outlinePrimary"} className="ml-2">
{t('edit')}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
<div className="flex items-center justify-end gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setSelected(r);
}}
>
<span>{t("viewSettings")}</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setSelected(r);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center justify-end">
<Link href={`/admin/api-keys/${r.id}`}>
<Button variant={"secondary"} className="ml-2" size="sm">
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
</div>
</div>
);
}
@@ -161,24 +160,23 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
dialog={
<div className="space-y-4">
<p>
{t('apiKeysQuestionRemove', {selectedApiKey: selected?.name || selected?.id})}
{t("apiKeysQuestionRemove", {
selectedApiKey:
selected?.name || selected?.id
})}
</p>
<p>
<b>
{t('apiKeysMessageRemove')}
</b>
<b>{t("apiKeysMessageRemove")}</b>
</p>
<p>
{t('apiKeysMessageConfirm')}
</p>
<p>{t("apiKeysMessageConfirm")}</p>
</div>
}
buttonText={t('apiKeysDeleteConfirm')}
buttonText={t("apiKeysDeleteConfirm")}
onConfirm={async () => deleteSite(selected!.id)}
string={selected.name}
title={t('apiKeysDelete')}
title={t("apiKeysDelete")}
/>
)}

View File

@@ -2,16 +2,7 @@ import { internal } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/lib/api/cookies";
import { SidebarSettings } from "@app/components/SidebarSettings";
import Link from "next/link";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import { GetApiKeyResponse } from "@server/routers/apiKeys";
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
import { HorizontalTabs } from "@app/components/HorizontalTabs";

View File

@@ -25,21 +25,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@app/components/ui/input";
import { InfoIcon } from "lucide-react";
import { Button } from "@app/components/ui/button";
import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios";
import { useParams, useRouter } from "next/navigation";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import Link from "next/link";
import { useRouter } from "next/navigation";
import {
CreateOrgApiKeyBody,
CreateOrgApiKeyResponse
@@ -108,7 +99,7 @@ export default function Page() {
const copiedForm = useForm<CopiedFormValues>({
resolver: zodResolver(copiedFormSchema),
defaultValues: {
copied: false
copied: true
}
});
@@ -299,54 +290,54 @@ export default function Page() {
</AlertDescription>
</Alert>
<h4 className="font-semibold">
{t('apiKeysInfo')}
</h4>
{/* <h4 className="font-semibold"> */}
{/* {t('apiKeysInfo')} */}
{/* </h4> */}
<CopyTextBox
text={`${apiKey.apiKeyId}.${apiKey.apiKey}`}
/>
<Form {...copiedForm}>
<form
className="space-y-4"
id="copied-form"
>
<FormField
control={copiedForm.control}
name="copied"
render={({ field }) => (
<FormItem>
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
defaultChecked={
copiedForm.getValues(
"copied"
) as boolean
}
onCheckedChange={(
e
) => {
copiedForm.setValue(
"copied",
e as boolean
);
}}
/>
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t('apiKeysConfirmCopy')}
</label>
</div>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
{/* <Form {...copiedForm}> */}
{/* <form */}
{/* className="space-y-4" */}
{/* id="copied-form" */}
{/* > */}
{/* <FormField */}
{/* control={copiedForm.control} */}
{/* name="copied" */}
{/* render={({ field }) => ( */}
{/* <FormItem> */}
{/* <div className="flex items-center space-x-2"> */}
{/* <Checkbox */}
{/* id="terms" */}
{/* defaultChecked={ */}
{/* copiedForm.getValues( */}
{/* "copied" */}
{/* ) as boolean */}
{/* } */}
{/* onCheckedChange={( */}
{/* e */}
{/* ) => { */}
{/* copiedForm.setValue( */}
{/* "copied", */}
{/* e as boolean */}
{/* ); */}
{/* }} */}
{/* /> */}
{/* <label */}
{/* htmlFor="terms" */}
{/* className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" */}
{/* > */}
{/* {t('apiKeysConfirmCopy')} */}
{/* </label> */}
{/* </div> */}
{/* <FormMessage /> */}
{/* </FormItem> */}
{/* )} */}
{/* /> */}
{/* </form> */}
{/* </Form> */}
</SettingsSectionBody>
</SettingsSection>
)}

View File

@@ -43,14 +43,14 @@ export default function IdpTable({ idps }: Props) {
try {
await api.delete(`/idp/${idpId}`);
toast({
title: t('success'),
description: t('idpDeletedDescription')
title: t("success"),
description: t("idpDeletedDescription")
});
setIsDeleteModalOpen(false);
router.refresh();
} catch (e) {
toast({
title: t('error'),
title: t("error"),
description: formatAxiosError(e),
variant: "destructive"
});
@@ -67,41 +67,6 @@ export default function IdpTable({ idps }: Props) {
};
const columns: ColumnDef<IdpRow>[] = [
{
id: "dots",
cell: ({ row }) => {
const r = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">{t('openMenu')}</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/admin/idp/${r.idpId}/general`}
>
<DropdownMenuItem>
{t('viewSettings')}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedIdp(r);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">{t('delete')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
},
{
accessorKey: "idpId",
header: ({ column }) => {
@@ -128,7 +93,7 @@ export default function IdpTable({ idps }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -144,7 +109,7 @@ export default function IdpTable({ idps }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('type')}
{t("type")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -162,9 +127,43 @@ export default function IdpTable({ idps }: Props) {
const siteRow = row.original;
return (
<div className="flex items-center justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link
className="block w-full"
href={`/admin/idp/${siteRow.idpId}/general`}
>
<DropdownMenuItem>
{t("viewSettings")}
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => {
setSelectedIdp(siteRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Link href={`/admin/idp/${siteRow.idpId}/general`}>
<Button variant={"outlinePrimary"} className="ml-2">
{t('edit')}
<Button
variant={"secondary"}
className="ml-2"
size="sm"
>
{t("edit")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
@@ -186,22 +185,20 @@ export default function IdpTable({ idps }: Props) {
dialog={
<div className="space-y-4">
<p>
{t('idpQuestionRemove', {name: selectedIdp.name})}
{t("idpQuestionRemove", {
name: selectedIdp.name
})}
</p>
<p>
<b>
{t('idpMessageRemove')}
</b>
</p>
<p>
{t('idpMessageConfirm')}
<b>{t("idpMessageRemove")}</b>
</p>
<p>{t("idpMessageConfirm")}</p>
</div>
}
buttonText={t('idpConfirmDelete')}
buttonText={t("idpConfirmDelete")}
onConfirm={async () => deleteIdp(selectedIdp.idpId)}
string={selectedIdp.name}
title={t('idpDelete')}
title={t("idpDelete")}
/>
)}

View File

@@ -4,17 +4,7 @@ import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/lib/api/cookies";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import { ProfessionalContentOverlay } from "@app/components/ProfessionalContentOverlay";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import { getTranslations } from "next-intl/server";
interface SettingsLayoutProps {

View File

@@ -140,7 +140,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
return (
<div className="flex items-center justify-end">
<Button
variant={"outlinePrimary"}
variant={"secondary"}
className="ml-2"
onClick={() => onEdit(policy)}
>

View File

@@ -9,7 +9,7 @@ import { internal } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { authCookieHeader } from "@app/lib/api/cookies";
import { Layout } from "@app/components/Layout";
import { adminNavItems } from "../navigation";
import { adminNavSections } from "../navigation";
export const dynamic = "force-dynamic";
@@ -47,7 +47,7 @@ export default async function AdminLayout(props: LayoutProps) {
return (
<UserProvider user={user}>
<Layout orgs={orgs} navItems={adminNavItems}>
<Layout orgs={orgs} navItems={adminNavSections}>
{props.children}
</Layout>
</UserProvider>

View File

@@ -122,7 +122,7 @@ export function LicenseKeysDataTable({
cell: ({ row }) => (
<div className="flex items-center justify-end space-x-2">
<Button
variant="outlinePrimary"
variant="secondary"
onClick={() => onDelete(row.original)}
>
{t('delete')}

View File

@@ -41,11 +41,11 @@ export default function UsersTable({ users }: Props) {
const deleteUser = (id: string) => {
api.delete(`/user/${id}`)
.catch((e) => {
console.error(t('userErrorDelete'), e);
console.error(t("userErrorDelete"), e);
toast({
variant: "destructive",
title: t('userErrorDelete'),
description: formatAxiosError(e, t('userErrorDelete'))
title: t("userErrorDelete"),
description: formatAxiosError(e, t("userErrorDelete"))
});
})
.then(() => {
@@ -84,7 +84,7 @@ export default function UsersTable({ users }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('username')}
{t("username")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -100,7 +100,7 @@ export default function UsersTable({ users }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('email')}
{t("email")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -116,7 +116,7 @@ export default function UsersTable({ users }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('name')}
{t("name")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -132,7 +132,7 @@ export default function UsersTable({ users }: Props) {
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t('identityProvider')}
{t("identityProvider")}
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
@@ -146,14 +146,15 @@ export default function UsersTable({ users }: Props) {
<>
<div className="flex items-center justify-end">
<Button
variant={"outlinePrimary"}
variant={"secondary"}
size="sm"
className="ml-2"
onClick={() => {
setSelected(r);
setIsDeleteModalOpen(true);
}}
>
{t('delete')}
{t("delete")}
</Button>
</div>
</>
@@ -174,26 +175,27 @@ export default function UsersTable({ users }: Props) {
dialog={
<div className="space-y-4">
<p>
{t('userQuestionRemove', {selectedUser: selected?.email || selected?.name || selected?.username})}
{t("userQuestionRemove", {
selectedUser:
selected?.email ||
selected?.name ||
selected?.username
})}
</p>
<p>
<b>
{t('userMessageRemove')}
</b>
<b>{t("userMessageRemove")}</b>
</p>
<p>
{t('userMessageConfirm')}
</p>
<p>{t("userMessageConfirm")}</p>
</div>
}
buttonText={t('userDeleteConfirm')}
buttonText={t("userDeleteConfirm")}
onConfirm={async () => deleteUser(selected!.id)}
string={
selected.email || selected.name || selected.username
}
title={t('userDeleteServer')}
title={t("userDeleteServer")}
/>
)}

View File

@@ -1,60 +0,0 @@
"use client";
import { Button } from "@app/components/ui/button";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useState } from "react";
import { useTranslations } from "next-intl";
export default function LicenseViolation() {
const { licenseStatus } = useLicenseStatusContext();
const [isDismissed, setIsDismissed] = useState(false);
const t = useTranslations();
if (!licenseStatus || isDismissed) return null;
// Show invalid license banner
if (licenseStatus.isHostLicensed && !licenseStatus.isLicenseValid) {
return (
<div className="fixed bottom-0 left-0 right-0 w-full bg-red-500 text-white p-4 text-center z-50">
<div className="flex justify-between items-center">
<p>
{t('componentsInvalidKey')}
</p>
<Button
variant={"ghost"}
className="hover:bg-yellow-500"
onClick={() => setIsDismissed(true)}
>
{t('dismiss')}
</Button>
</div>
</div>
);
}
// Show usage violation banner
if (
licenseStatus.maxSites &&
licenseStatus.usedSites &&
licenseStatus.usedSites > licenseStatus.maxSites
) {
return (
<div className="fixed bottom-0 left-0 right-0 w-full bg-yellow-500 text-black p-4 text-center z-50">
<div className="flex justify-between items-center">
<p>
{t('componentsLicenseViolation', {usedSites: licenseStatus.usedSites, maxSites: licenseStatus.maxSites})}
</p>
<Button
variant={"ghost"}
className="hover:bg-yellow-500"
onClick={() => setIsDismissed(true)}
>
{t('dismiss')}
</Button>
</div>
</div>
);
}
return null;
}

View File

@@ -1,99 +0,0 @@
"use client";
import { useState } from "react";
import {
Card,
CardHeader,
CardTitle,
CardContent,
CardDescription
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { ArrowRight, Plus } from "lucide-react";
import { useTranslations } from "next-intl";
interface Organization {
id: string;
name: string;
}
interface OrganizationLandingProps {
organizations?: Organization[];
disableCreateOrg?: boolean;
}
export default function OrganizationLanding({
organizations = [],
disableCreateOrg = false
}: OrganizationLandingProps) {
const [selectedOrg, setSelectedOrg] = useState<string | null>(null);
const handleOrgClick = (orgId: string) => {
setSelectedOrg(orgId);
};
const t = useTranslations();
function getDescriptionText() {
if (organizations.length === 0) {
if (!disableCreateOrg) {
return t('componentsErrorNoMemberCreate');
} else {
return t('componentsErrorNoMember');
}
}
return t('componentsMember', {count: organizations.length});
}
return (
<Card>
<CardHeader>
<CardTitle>{t('welcome')}</CardTitle>
<CardDescription>{getDescriptionText()}</CardDescription>
</CardHeader>
<CardContent>
{organizations.length === 0 ? (
disableCreateOrg ? (
<p className="text-center text-muted-foreground">
t('componentsErrorNoMember')
</p>
) : (
<Link href="/setup">
<Button
className="w-full h-auto py-3 text-lg"
size="lg"
>
<Plus className="mr-2 h-5 w-5" />
{t('componentsCreateOrg')}
</Button>
</Link>
)
) : (
<ul className="space-y-2">
{organizations.map((org) => (
<li key={org.id}>
<Link href={`/${org.id}/settings`}>
<Button
variant="outline"
className={`flex items-center justify-between w-full h-auto py-3 ${
selectedOrg === org.id
? "ring-2 ring-primary"
: ""
}`}
>
<div className="truncate">
{org.name}
</div>
<ArrowRight size={20} />
</Button>
</Link>
</li>
))}
</ul>
)}
</CardContent>
</Card>
);
}

View File

@@ -1,42 +0,0 @@
"use client";
import React from "react";
import confetti from "canvas-confetti";
import { Star } from "lucide-react";
import { useTranslations } from 'next-intl';
export default function SupporterMessage({ tier }: { tier: string }) {
const t = useTranslations();
return (
<div className="relative flex items-center space-x-2 whitespace-nowrap group">
<span
className="cursor-pointer"
onClick={(e) => {
// Get the bounding box of the element
const rect = (
e.target as HTMLElement
).getBoundingClientRect();
// Trigger confetti centered on the word "Pangolin"
confetti({
particleCount: 100,
spread: 70,
origin: {
x: (rect.left + rect.width / 2) / window.innerWidth,
y: rect.top / window.innerHeight
},
colors: ["#FFA500", "#FF4500", "#FFD700"]
});
}}
>
Pangolin
</span>
<Star className="w-3 h-3"/>
<div className="absolute left-1/2 transform -translate-x-1/2 -top-10 hidden group-hover:block text-primary text-sm rounded-md border shadow-md px-4 py-2 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
{t('componentsSupporterMessage', {tier: tier})}
</div>
</div>
);
}

View File

@@ -1,125 +1,139 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Space+Grotesk:wght@300..700&display=swap");
@import 'tw-animate-css';
@import 'tailwindcss';
@import "tw-animate-css";
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
:root {
--background: hsl(0 0% 98%);
--foreground: hsl(20 0% 10%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(20 0% 10%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(20 0% 10%);
--primary: hsl(24.6 95% 53.1%);
--primary-foreground: hsl(60 9.1% 97.8%);
--secondary: hsl(60 4.8% 95.9%);
--secondary-foreground: hsl(24 9.8% 10%);
--muted: hsl(60 4.8% 85%);
--muted-foreground: hsl(25 5.3% 44.7%);
--accent: hsl(60 4.8% 90%);
--accent-foreground: hsl(24 9.8% 10%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(60 9.1% 97.8%);
--border: hsl(20 5.9% 90%);
--input: hsl(20 5.9% 75%);
--ring: hsl(20 5.9% 75%);
--radius: 0.75rem;
--chart-1: hsl(12 76% 61%);
--chart-2: hsl(173 58% 39%);
--chart-3: hsl(197 37% 24%);
--chart-4: hsl(43 74% 66%);
--chart-5: hsl(27 87% 67%);
--radius: 0.65rem;
--background: oklch(0.99 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.6717 0.1946 41.93);
--primary-foreground: oklch(0.98 0.016 73.684);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.213 47.604);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.705 0.213 47.604);
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.213 47.604);
}
.dark {
--background: hsl(20 0% 8%);
--foreground: hsl(60 9.1% 97.8%);
--card: hsl(20 0% 10%);
--card-foreground: hsl(60 9.1% 97.8%);
--popover: hsl(20 0% 10%);
--popover-foreground: hsl(60 9.1% 97.8%);
--primary: hsl(20.5 90.2% 48.2%);
--primary-foreground: hsl(60 9.1% 97.8%);
--secondary: hsl(12 6.5% 15%);
--secondary-foreground: hsl(60 9.1% 97.8%);
--muted: hsl(12 6.5% 25%);
--muted-foreground: hsl(24 5.4% 63.9%);
--accent: hsl(12 2.5% 15%);
--accent-foreground: hsl(60 9.1% 97.8%);
--destructive: hsl(0 72.2% 50.6%);
--destructive-foreground: hsl(60 9.1% 97.8%);
--border: hsl(12 6.5% 15%);
--input: hsl(12 6.5% 35%);
--ring: hsl(12 6.5% 35%);
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
--background: oklch(0.20 0.006 285.885);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.6717 0.1946 41.93);
--primary-foreground: oklch(0.98 0.016 73.684);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.646 0.222 41.116);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.646 0.222 41.116);
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.646 0.222 41.116);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
@layer base {
* {
@apply border-border;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
body {
@apply bg-background text-foreground;
}
}
p {
word-break: keep-all;
white-space: normal;
word-break: keep-all;
white-space: normal;
}

View File

@@ -11,7 +11,7 @@ import { AxiosResponse } from "axios";
import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey";
import LicenseStatusProvider from "@app/providers/LicenseStatusProvider";
import { GetLicenseStatusResponse } from "@server/routers/license";
import LicenseViolation from "./components/LicenseViolation";
import LicenseViolation from "@app/components/LicenseViolation";
import { cache } from "react";
import { NextIntlClientProvider } from "next-intl";
import { getLocale } from "next-intl/server";

View File

@@ -9,94 +9,97 @@ import {
Fingerprint,
Workflow,
KeyRound,
TicketCheck
TicketCheck,
User
} from "lucide-react";
export const orgLangingNavItems: SidebarNavItem[] = [
{
title: "sidebarOverview",
href: "/{orgId}",
icon: <Home className="h-4 w-4" />
}
];
export type SidebarNavSection = {
heading: string;
items: SidebarNavItem[];
};
export const rootNavItems: SidebarNavItem[] = [
export const orgNavSections: SidebarNavSection[] = [
{
title: "sidebarHome",
href: "/",
icon: <Home className="h-4 w-4" />
}
];
export const orgNavItems: SidebarNavItem[] = [
{
title: "sidebarSites",
href: "/{orgId}/settings/sites",
icon: <Combine className="h-4 w-4" />
},
{
title: "sidebarResources",
href: "/{orgId}/settings/resources",
icon: <Waypoints className="h-4 w-4" />
},
{
title: "sidebarAccessControl",
href: "/{orgId}/settings/access",
icon: <Users className="h-4 w-4" />,
autoExpand: true,
children: [
heading: "General",
items: [
{
title: "sidebarUsers",
href: "/{orgId}/settings/access/users",
children: [
{
title: "sidebarInvitations",
href: "/{orgId}/settings/access/invitations"
}
]
title: "sidebarSites",
href: "/{orgId}/settings/sites",
icon: <Combine className="h-4 w-4" />
},
{
title: "sidebarRoles",
href: "/{orgId}/settings/access/roles"
title: "sidebarResources",
href: "/{orgId}/settings/resources",
icon: <Waypoints className="h-4 w-4" />
}
]
},
{
title: "sidebarShareableLinks",
href: "/{orgId}/settings/share-links",
icon: <LinkIcon className="h-4 w-4" />
heading: "Access Control",
items: [
{
title: "sidebarUsers",
href: "/{orgId}/settings/access/users",
icon: <User className="h-4 w-4" />
},
{
title: "sidebarRoles",
href: "/{orgId}/settings/access/roles",
icon: <Users className="h-4 w-4" />
},
{
title: "sidebarInvitations",
href: "/{orgId}/settings/access/invitations",
icon: <TicketCheck className="h-4 w-4" />
},
{
title: "sidebarShareableLinks",
href: "/{orgId}/settings/share-links",
icon: <LinkIcon className="h-4 w-4" />
}
]
},
{
title: "sidebarApiKeys",
href: "/{orgId}/settings/api-keys",
icon: <KeyRound className="h-4 w-4" />
},
{
title: "sidebarSettings",
href: "/{orgId}/settings/general",
icon: <Settings className="h-4 w-4" />
heading: "Organization",
items: [
{
title: "sidebarApiKeys",
href: "/{orgId}/settings/api-keys",
icon: <KeyRound className="h-4 w-4" />
},
{
title: "sidebarSettings",
href: "/{orgId}/settings/general",
icon: <Settings className="h-4 w-4" />
}
]
}
];
export const adminNavItems: SidebarNavItem[] = [
export const adminNavSections: SidebarNavSection[] = [
{
title: "sidebarAllUsers",
href: "/admin/users",
icon: <Users className="h-4 w-4" />
},
{
title: "sidebarApiKeys",
href: "/admin/api-keys",
icon: <KeyRound className="h-4 w-4" />
},
{
title: "sidebarIdentityProviders",
href: "/admin/idp",
icon: <Fingerprint className="h-4 w-4" />
},
{
title: "sidebarLicense",
href: "/admin/license",
icon: <TicketCheck className="h-4 w-4" />
heading: "Admin",
items: [
{
title: "sidebarAllUsers",
href: "/admin/users",
icon: <Users className="h-4 w-4" />
},
{
title: "sidebarApiKeys",
href: "/admin/api-keys",
icon: <KeyRound className="h-4 w-4" />
},
{
title: "sidebarIdentityProviders",
href: "/admin/idp",
icon: <Fingerprint className="h-4 w-4" />
},
{
title: "sidebarLicense",
href: "/admin/license",
icon: <TicketCheck className="h-4 w-4" />
}
]
}
];

View File

@@ -6,11 +6,10 @@ import { ListUserOrgsResponse } from "@server/routers/org";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { cache } from "react";
import OrganizationLanding from "./components/OrganizationLanding";
import OrganizationLanding from "@app/components/OrganizationLanding";
import { pullEnv } from "@app/lib/pullEnv";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import { Layout } from "@app/components/Layout";
import { rootNavItems } from "./navigation";
import { InitialSetupCompleteResponse } from "@server/routers/auth";
import { cookies } from "next/headers";
@@ -73,17 +72,15 @@ export default async function Page(props: {
}
}
// Check for pangolin-last-org cookie and redirect if valid
const allCookies = await cookies();
const lastOrgCookie = allCookies.get("pangolin-last-org")?.value;
if (lastOrgCookie && orgs.length > 0) {
// Check if the last org from cookie exists in user's organizations
const lastOrgExists = orgs.some(org => org.orgId === lastOrgCookie);
const lastOrgExists = orgs.some((org) => org.orgId === lastOrgCookie);
if (lastOrgExists) {
redirect(`/${lastOrgCookie}`);
} else {
const ownedOrg = orgs.find(org => org.isOwner);
const ownedOrg = orgs.find((org) => org.isOwner);
if (ownedOrg) {
redirect(`/${ownedOrg.orgId}`);
} else {
@@ -94,7 +91,7 @@ export default async function Page(props: {
return (
<UserProvider user={user}>
<Layout orgs={orgs} navItems={rootNavItems} showBreadcrumbs={false}>
<Layout orgs={orgs} navItems={[]}>
<div className="w-full max-w-md mx-auto md:mt-32 mt-4">
<OrganizationLanding
disableCreateOrg={

View File

@@ -6,7 +6,6 @@ import UserProvider from "@app/providers/UserProvider";
import { Metadata } from "next";
import { redirect } from "next/navigation";
import { cache } from "react";
import { rootNavItems } from "../navigation";
import { ListUserOrgsResponse } from "@server/routers/org";
import { internal } from "@app/lib/api";
import { AxiosResponse } from "axios";
@@ -54,11 +53,7 @@ export default async function SetupLayout({
return (
<>
<UserProvider user={user}>
<Layout
navItems={rootNavItems}
showBreadcrumbs={false}
orgs={orgs}
>
<Layout navItems={[]} orgs={orgs}>
<div className="w-full max-w-2xl mx-auto md:mt-32 mt-4">
{children}
</div>