mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-11 15:36:38 +00:00
clean up ui pass 1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -201,7 +201,7 @@ export default function RegenerateInvitationForm({
|
||||
setValidHours(parseInt(value))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t('inviteValidityPeriodSelect')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -392,7 +392,7 @@ export default function CreateShareLinkForm({
|
||||
defaultValue={field.value.toString()}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t('selectDuration')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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")}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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")}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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")}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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" />
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user